diff --git a/.gitignore b/.gitignore index ea8c4bf..fedaa2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.env diff --git a/Cargo.lock b/Cargo.lock index 216241f..639be26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -924,6 +924,15 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.101" @@ -948,6 +957,35 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1439,6 +1477,7 @@ dependencies = [ "anyhow", "chrono", "clap", + "rand", "reqwest", "serde", "serde_json", @@ -1891,6 +1930,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index d6589ab..4b452cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" anyhow = "1.0.100" chrono = { version = "0.4.42", features = ["serde"] } clap = { version = "4.5.49", features = ["derive"] } +rand = "0.9.2" reqwest = { version = "0.12.24", features = ["gzip", "json"] } serde = "1.0.228" serde_json = "1.0.145" diff --git a/mise.toml b/mise.toml index 5c2df98..73f304f 100644 --- a/mise.toml +++ b/mise.toml @@ -1,2 +1,5 @@ +[env] +_.file = '.env' + [tasks.dev] run = "cargo run" diff --git a/src/jellyfin.rs b/src/jellyfin.rs index 208664b..1487383 100644 --- a/src/jellyfin.rs +++ b/src/jellyfin.rs @@ -1,12 +1,11 @@ use anyhow::anyhow; +use rand::seq::IteratorRandom; use reqwest::header::{HeaderMap, HeaderValue}; -use crate::jellyfin::models::GetItemsResponseModel; +use crate::jellyfin::models::{GetItemsQueryString, GetItemsResponseModel, ItemModel}; pub mod models; -static JELLYFIN_VGM_FOLDER: &str = ""; - pub struct Client { _client: reqwest::Client, host: String, @@ -18,7 +17,7 @@ impl Client { let mut headers = HeaderMap::new(); let auth_str = format!( r#"MediaBrowser Client="Jellyfin Web", Device="Firefox", DeviceId="TW96aWxsYS81LjAgKFgxMTsgTGludXggeDg2XzY0OyBydjo5NC4wKSBHZWNrby8yMDEwMDEwMSBGaXJlZm94Lzk0LjB8MTYzODA1MzA2OTY4Mw11", Version="10.7.6", Token="{}""#, - "e9d4caa8c94f429280b43ba45a50ba6b" + std::env::var("JELLYFIN_API_KEY")? ); headers.insert("Authorization", HeaderValue::from_str(auth_str.as_str())?); @@ -32,24 +31,24 @@ impl Client { Ok(Self { _client, host: host.into(), - user_id: String::from("4a80af8c258e417e98f86e870e17d929"), + user_id: std::env::var("JELLYFIN_USER_ID")?, }) } pub async fn get_items + std::fmt::Display>( &self, parent_id: T, + query: GetItemsQueryString, ) -> anyhow::Result { // INFO: needs to be created manually, escaped characters mess with Jellyfin server - let query = format!( - "parentId={parent_id}&sort_by=SortName&fields=Path,RecursiveItemCount&enable_images=false" - ); let response = self ._client .get(format!( "{}/Users/{}/items?{}", - self.host, self.user_id, query + self.host, + self.user_id, + query.build(parent_id) )) .send() .await?; @@ -69,4 +68,42 @@ impl Client { Ok(context) } + + pub async fn get_random_vgm_album(&self) -> anyhow::Result { + let response_folders = self + .get_items( + std::env::var("JELLYFIN_VGM_FOLDER")?, + GetItemsQueryString::Folder, + ) + .await?; + + let mut rng = rand::rng(); + + let folder = response_folders + .items + .into_iter() + .filter(|item| item.recursive_item_count > 0) + .choose(&mut rng) + .ok_or(anyhow!("Failed to get a VGM folder from Jellyfin!"))?; + + let response_albums = self + .get_items(&folder.id, GetItemsQueryString::Album) + .await?; + + let folder = response_albums + .items + .into_iter() + .filter(|item| item.recursive_item_count > 0) + .choose(&mut rng) + .ok_or(anyhow!("Failed to get a VGM album from Jellyfin!"))?; + + Ok(folder) + } + + pub async fn get_songs + std::fmt::Display>( + &self, + album_id: T, + ) -> anyhow::Result { + self.get_items(album_id, GetItemsQueryString::Songs).await + } } diff --git a/src/jellyfin/models.rs b/src/jellyfin/models.rs index 0607b2a..1ec8022 100644 --- a/src/jellyfin/models.rs +++ b/src/jellyfin/models.rs @@ -1,12 +1,22 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetItemsQueryStringModel { - pub parent_id: String, - pub sort_by: String, - pub fields: String, - pub enable_images: bool, +pub enum GetItemsQueryString { + Folder, + Album, + Songs, +} + +impl GetItemsQueryString { + pub fn build(&self, parent_id: impl Into + std::fmt::Display) -> String { + let common = format!("parentId={parent_id}&enable_images=false"); + match self { + Self::Folder => format!("{common}&SortBy=SortName&fields=Path,RecursiveItemCount"), + Self::Album => format!("{common}&SortBy=SortName&fields=Path,RecursiveItemCount"), + Self::Songs => format!( + "{common}&SortBy=ParentIndexNumber,IndexNumber,SortName&fields=ItemCounts,MediaSourceCount,Path" + ), + } + } } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -32,6 +42,7 @@ pub struct ItemModel { pub album_artist: Option, pub album_artists: Option>, pub media_type: String, + #[serde(default)] pub recursive_item_count: u64, } diff --git a/src/main.rs b/src/main.rs index 0c3c7c0..3bb34d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,8 +12,9 @@ async fn main() -> anyhow::Result<()> { let jelly = jellyfin::Client::new("https://media.hoshikusu.xyz")?; - let items = jelly.get_items("574563ff9a41f2c98c234db69157cc87").await?; - println!("{:#?}", items); + let item = jelly.get_random_vgm_album().await?; + let songs = jelly.get_songs(item.id).await?; + println!("{:#?}", songs); Ok(()) }