aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--common/src/lib.rs4
-rw-r--r--server/src/routes/ui/browser.rs8
-rw-r--r--server/src/routes/ui/node.rs47
-rw-r--r--server/src/routes/ui/style/directorypage.css24
-rw-r--r--tools/src/bin/import.rs106
-rw-r--r--tools/src/tmdb.rs32
6 files changed, 144 insertions, 77 deletions
diff --git a/common/src/lib.rs b/common/src/lib.rs
index 2e80744..fd69d8f 100644
--- a/common/src/lib.rs
+++ b/common/src/lib.rs
@@ -20,6 +20,8 @@ pub struct CommmonInfo {
pub poster: Option<PathBuf>,
#[serde(default)]
pub backdrop: Option<PathBuf>,
+ #[serde(default)]
+ pub index: Option<usize>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
@@ -38,8 +40,6 @@ pub struct ItemInfo {
pub tracks: BTreeMap<usize, SourceTrack>,
}
-
-
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DirectoryKind {
diff --git a/server/src/routes/ui/browser.rs b/server/src/routes/ui/browser.rs
index d0c09b1..30eb3f2 100644
--- a/server/src/routes/ui/browser.rs
+++ b/server/src/routes/ui/browser.rs
@@ -3,7 +3,7 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2023 metamuffin <metamuffin.org>
*/
-use super::{account::session::Session, error::MyError, layout::DynLayoutPage, node::ItemCard};
+use super::{account::session::Session, error::MyError, layout::DynLayoutPage, node::PosterCard};
use crate::library::{Library, Node};
use rocket::{get, State};
use std::collections::VecDeque;
@@ -26,7 +26,11 @@ pub fn r_all_items(_sess: Session, library: &State<Library>) -> Result<DynLayout
.page.dir {
h1 { "All Items" }
ul.directorylisting { @for item in &items {
- li { @ItemCard { item: &item } }
+ li {@PosterCard {
+ wide: false, dir: false,
+ path: item.lib_path.clone(),
+ title: &item.info.title
+ }}
}}
}
},
diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs
index d5e52ff..b9e9871 100644
--- a/server/src/routes/ui/node.rs
+++ b/server/src/routes/ui/node.rs
@@ -15,6 +15,7 @@ use crate::{
CONF,
};
use anyhow::Context;
+use jellycommon::DirectoryKind;
use log::info;
use rocket::{get, http::ContentType, State};
use rocket::{FromFormField, UriDisplayQuery};
@@ -49,23 +50,17 @@ markup::define! {
}
NodeCard<'a>(node: &'a Arc<Node>) {
@match node.as_ref() {
- Node::Directory(dir) => { @DirectoryCard { dir } }
- Node::Item(item) => { @ItemCard { item } }
- }
- }
- DirectoryCard<'a>(dir: &'a Arc<Directory>) {
- div.card.dir {
- div.banner {
- a[href=uri!(r_library_node(&dir.lib_path))] {
- img[src=uri!(r_item_assets(&dir.lib_path, AssetRole::Poster))];
- }
- div.hover { a[href=uri!(r_library_node(&dir.lib_path))] { "Open" } }
- }
- p.title {
- a[href=uri!(r_library_node(&dir.lib_path))] {
- @dir.info.title
- }
- }
+ Node::Directory(dir) => {@PosterCard {
+ wide: !matches!(dir.info.kind, DirectoryKind::Series | DirectoryKind::Season),
+ dir: true,
+ path: dir.lib_path.clone(),
+ title: &dir.info.title
+ }}
+ Node::Item(item) => {@PosterCard {
+ wide: false, dir: false,
+ path: item.lib_path.clone(),
+ title: &item.info.title
+ }}
}
}
DirectoryPage<'a>(dir: &'a Arc<Directory>) {
@@ -81,17 +76,21 @@ markup::define! {
}
}
}
- ItemCard<'a>(item: &'a Arc<Item>) {
- div.card.item {
+ PosterCard<'a>(path: PathBuf, title: &'a str, wide: bool, dir: bool) {
+ div[class=if *wide {"card wide poster"} else {"card poster"}] {
div.banner {
- a[href=uri!(r_library_node(&item.lib_path))] {
- img[src=uri!(r_item_assets(&item.lib_path, AssetRole::Poster))];
+ a[href=uri!(r_library_node(path))] {
+ img[src=uri!(r_item_assets(path, AssetRole::Poster))];
+ }
+ @if *dir {
+ div.hoverdir { a[href=&uri!(r_library_node(path))] { "Open" } }
+ } else {
+ div.hoveritem { a[href=&player_uri(path)] { "▶" } }
}
- div.hover { a[href=&player_uri(&item.lib_path)] { "▶" } }
}
p.title {
- a[href=uri!(r_library_node(&item.lib_path))] {
- @item.info.title
+ a[href=uri!(r_library_node(path))] {
+ @title
}
}
}
diff --git a/server/src/routes/ui/style/directorypage.css b/server/src/routes/ui/style/directorypage.css
index e81dce7..0d18c82 100644
--- a/server/src/routes/ui/style/directorypage.css
+++ b/server/src/routes/ui/style/directorypage.css
@@ -37,10 +37,10 @@
padding: 1em;
height: var(--card-size);
}
-.card.item {
+.card.poster {
width: calc(var(--card-size) / var(--item-banner-aspect));
}
-.card.dir {
+.card.poster.wide {
width: calc(var(--card-size) / var(--dir-banner-aspect));
}
@@ -56,11 +56,11 @@
grid-area: 1 / 1;
}
-.card.item .banner img {
+.card.poster .banner img {
width: calc(var(--card-size) / var(--item-banner-aspect));
height: var(--card-size);
}
-.card.dir .banner img {
+.card.poster.wide .banner img {
width: calc(var(--card-size) / var(--dir-banner-aspect));
height: var(--card-size);
}
@@ -70,7 +70,7 @@
}
-.card.dir .banner .hover {
+.card.poster .banner .hoverdir {
transition: opacity 0.3s, backdrop-filter 0.3s;
opacity: 0;
display: flex;
@@ -79,12 +79,12 @@
height: 5em;
margin-top: -5em;
}
-.card.dir .banner:hover .hover {
+.card.poster .banner:hover .hoverdir {
opacity: 1;
background-color: #0004;
backdrop-filter: blur(3px);
}
-.card.dir .banner .hover a {
+.card.poster .banner .hoverdir a {
text-decoration: none;
width: 100%;
height: 1.7em;
@@ -97,11 +97,11 @@
margin: 0.6em;
transition: background-color 0.2s;
}
-.card.dir .banner .hover a:hover {
+.card.poster .banner .hoverdir a:hover {
background-color: #0008;
}
-.card.item .banner .hover {
+.card.poster .banner .hoveritem {
pointer-events: none;
grid-area: 1 / 1;
transition: opacity 0.3s, backdrop-filter 0.3s;
@@ -110,12 +110,12 @@
justify-content: center;
align-items: center;
}
-.card.item .banner:hover .hover {
+.card.poster .banner:hover .hoveritem {
opacity: 1;
background-color: #0004;
backdrop-filter: blur(3px);
}
-.card.item .banner .hover a {
+.card.poster .banner .hoveritem a {
text-decoration: none;
font-stretch: 200%;
width: 1em;
@@ -129,7 +129,7 @@
background-color: #0005;
transition: background-color 0.2s, font-size 0.2s;
}
-.card.item .banner .hover a:hover {
+.card.poster .banner .hoveritem a:hover {
background-color: #0008;
font-size: 2.4em;
}
diff --git a/tools/src/bin/import.rs b/tools/src/bin/import.rs
index c771450..3050f71 100644
--- a/tools/src/bin/import.rs
+++ b/tools/src/bin/import.rs
@@ -5,7 +5,7 @@ Copyright (C) 2023 metamuffin <metamuffin.org>
*/
use anyhow::Context;
use clap::{Parser, Subcommand};
-use jellycommon::{CommmonInfo, ItemInfo};
+use jellycommon::{CommmonInfo, DirectoryInfo, ItemInfo};
use jellymatroska::read::EbmlReader;
use jellyremuxer::import::import_read;
use jellytools::tmdb::{tmdb_details, tmdb_image, tmdb_search};
@@ -13,7 +13,7 @@ use log::{info, warn};
use std::{
fs::File,
io::{stdin, Write},
- path::PathBuf,
+ path::{Path, PathBuf},
process::exit,
};
@@ -32,7 +32,15 @@ enum Action {
title: Option<String>,
#[arg(short = 'T', long)]
tmdb: Option<String>,
- #[arg(short = 'i', long)]
+ #[arg(short, long)]
+ input: Option<PathBuf>,
+ #[arg(short, long)]
+ series: bool,
+ },
+ Episode {
+ path: PathBuf,
+ index: usize,
+ title: String,
input: PathBuf,
},
Set {
@@ -66,23 +74,26 @@ fn main() -> anyhow::Result<()> {
title,
tmdb: id,
input,
+ series,
} => {
+ assert!(series || input.is_some(), "series or input required");
+ let kind = if series { "tv" } else { "movie" };
let key = std::env::var("TMDB_API_KEY").context("tmdb api key required")?;
let id = if let Some(id) = id {
id.parse().unwrap()
} else {
let title = title.unwrap();
- let results = tmdb_search(&title, &key)?;
+ let results = tmdb_search(kind, &title, &key)?;
info!("results:");
for (i, r) in results.results.iter().enumerate() {
info!(
"\t[{i}] {}: {} ({})",
r.id,
- r.title,
+ r.title.as_ref().or(r.name.as_ref()).unwrap(),
r.overview.chars().take(100).collect::<String>()
);
}
- let res_index = if results.results.len() != 1 {
+ let res_index = if results.results.len() > 1 {
stdin()
.lines()
.next()
@@ -96,8 +107,8 @@ fn main() -> anyhow::Result<()> {
results.results[res_index].id
};
- let details = tmdb_details(id, &key).context("fetching details")?;
- let ident = make_ident(&details.title);
+ let details = tmdb_details(kind, id, &key).context("fetching details")?;
+ let ident = make_ident(details.title.as_ref().or(details.name.as_ref()).unwrap());
let path = path.join(&ident);
std::fs::create_dir_all(&path)?;
@@ -125,36 +136,82 @@ fn main() -> anyhow::Result<()> {
backdrop,
description: Some(details.overview),
tagline: details.tagline,
- title: details.title,
+ title: details.title.clone().or(details.name.clone()).unwrap(),
+ index: None,
};
- let mut iteminfo = ItemInfo {
- common,
- duration: Default::default(),
- tracks: Default::default(),
- };
- info!("{iteminfo:#?}");
info!("is this correct? [y/n]");
if stdin().lines().next().unwrap().unwrap() != "y" {
exit(0)
}
+ let k = if let Some(input) = input {
+ let mut iteminfo = ItemInfo {
+ common,
+ duration: Default::default(),
+ tracks: Default::default(),
+ };
+ info!("{iteminfo:#?}");
+ let source_path = path.join(format!("source.mkv"));
+ // std::fs::rename(&input, &source_path)?;
+ // std::os::unix::fs::symlink(&input, &source_path)?;
+ std::fs::copy(&input, &source_path)?;
+ import_source(&mut iteminfo, &source_path)?;
+ serde_json::to_string_pretty(&iteminfo)?
+ } else {
+ serde_json::to_string_pretty(&DirectoryInfo {
+ common,
+ kind: jellycommon::DirectoryKind::Series,
+ })?
+ };
+
+ if args.dry {
+ println!("{k}")
+ } else {
+ let mut f = File::create(path.join(if series {
+ "directory.json".to_string()
+ } else {
+ format!("{ident}.jelly",)
+ }))?;
+ f.write_all(k.as_bytes())?;
+ }
+
+ Ok(())
+ }
+ Action::Episode {
+ path,
+ index,
+ title,
+ input,
+ } => {
+ let ident = make_ident(&title);
+ let common = CommmonInfo {
+ poster: None,
+ backdrop: None,
+ description: None,
+ tagline: None,
+ title,
+ index: Some(index),
+ };
+ let mut iteminfo = ItemInfo {
+ common,
+ duration: Default::default(),
+ tracks: Default::default(),
+ };
+ let path = path.join(&ident);
+ std::fs::create_dir_all(&path)?;
let source_path = path.join(format!("source.mkv"));
// std::fs::rename(&input, &source_path)?;
// std::os::unix::fs::symlink(&input, &source_path)?;
std::fs::copy(&input, &source_path)?;
- let input = File::open(&source_path).unwrap();
- let mut input = EbmlReader::new(input);
- import_read(&source_path, &mut input, &mut iteminfo)?;
-
+ import_source(&mut iteminfo, &source_path)?;
let k = serde_json::to_string_pretty(&iteminfo)?;
if args.dry {
println!("{k}")
} else {
- let mut f = File::create(path.join(format!("{ident}.jelly")))?;
+ let mut f = File::create(path.join(format!("{ident}.jelly",)))?;
f.write_all(k.as_bytes())?;
}
-
Ok(())
}
Action::Set {
@@ -178,6 +235,7 @@ fn main() -> anyhow::Result<()> {
tagline: None,
description: None,
title: item.to_str().unwrap().to_string(),
+ index: None,
},
duration: 0.0,
tracks: Default::default(),
@@ -231,3 +289,9 @@ fn make_ident(s: &str) -> String {
}
out
}
+fn import_source(iteminfo: &mut ItemInfo, source_path: &Path) -> anyhow::Result<()> {
+ let input = File::open(&source_path).unwrap();
+ let mut input = EbmlReader::new(input);
+ import_read(&source_path.to_path_buf(), &mut input, iteminfo)?;
+ Ok(())
+}
diff --git a/tools/src/tmdb.rs b/tools/src/tmdb.rs
index 6f8c341..5f21afd 100644
--- a/tools/src/tmdb.rs
+++ b/tools/src/tmdb.rs
@@ -21,14 +21,14 @@ pub struct TmdbQueryResult {
pub backdrop_path: Option<String>,
pub genre_ids: Vec<u64>,
pub id: u64,
- pub original_language: String,
- pub original_title: String,
+ pub original_language: Option<String>,
+ pub original_title: Option<String>,
pub overview: String,
pub popularity: f64,
pub poster_path: Option<String>,
- pub release_date: String,
- pub title: String,
- pub video: bool,
+ pub release_date: Option<String>,
+ pub title: Option<String>,
+ pub name: Option<String>,
pub vote_average: f64,
pub vote_count: usize,
}
@@ -39,21 +39,21 @@ pub struct TmdbDetails {
pub backdrop_path: Option<String>,
pub genres: Vec<TmdbGenre>,
pub id: u64,
- pub original_language: String,
- pub original_title: String,
+ pub original_language: Option<String>,
+ pub original_title: Option<String>,
pub overview: String,
pub popularity: f64,
pub poster_path: Option<String>,
- pub release_date: String,
- pub title: String,
- pub video: bool,
+ pub release_date: Option<String>,
+ pub title: Option<String>,
+ pub name: Option<String>,
pub vote_average: f64,
pub vote_count: usize,
- pub budget: usize,
+ pub budget: Option<usize>,
pub homepage: Option<String>,
pub imdb_id: Option<String>,
pub production_companies: Vec<TmdbProductionCompany>,
- pub revenue: usize,
+ pub revenue: Option<usize>,
pub tagline: Option<String>,
}
@@ -70,19 +70,19 @@ pub struct TmdbProductionCompany {
pub logo_path: Option<String>,
}
-pub fn tmdb_search(query: &str, key: &str) -> anyhow::Result<TmdbQuery> {
+pub fn tmdb_search(kind: &str, query: &str, key: &str) -> anyhow::Result<TmdbQuery> {
info!("searching tmdb: {query:?}");
Ok(reqwest::blocking::get(&format!(
- "https://api.themoviedb.org/3/search/movie?query={}&api_key={key}",
+ "https://api.themoviedb.org/3/search/{kind}?query={}&api_key={key}",
query.replace(" ", "+")
))?
.json::<TmdbQuery>()?)
}
-pub fn tmdb_details(id: u64, key: &str) -> anyhow::Result<TmdbDetails> {
+pub fn tmdb_details(kind: &str, id: u64, key: &str) -> anyhow::Result<TmdbDetails> {
info!("fetching details: {id:?}");
Ok(reqwest::blocking::get(&format!(
- "https://api.themoviedb.org/3/movie/{id}?api_key={key}"
+ "https://api.themoviedb.org/3/{kind}/{id}?api_key={key}"
))?
.json()?)
}