a Jellyfin & Subsonic client for the terminal — powered by mpv, Chromecast and UPnP MediaRenderer
mpv
chromecast
mpris
navidrome
jellyfin
upnp
tui
7.4 kB
262 lines
1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4#[serde(rename_all = "PascalCase")]
5pub struct AuthResult {
6 pub access_token: String,
7 pub server_id: String,
8 pub user: AuthUser,
9}
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(rename_all = "PascalCase")]
13pub struct AuthUser {
14 pub id: String,
15 pub name: String,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum ItemKind {
20 Audio,
21 MusicAlbum,
22 MusicArtist,
23 Playlist,
24 Movie,
25 Series,
26 Season,
27 Episode,
28 Video,
29 Folder,
30 Other,
31}
32
33impl ItemKind {
34 pub fn parse(s: &str) -> Self {
35 match s {
36 "Audio" => Self::Audio,
37 "MusicAlbum" => Self::MusicAlbum,
38 "MusicArtist" => Self::MusicArtist,
39 "Playlist" => Self::Playlist,
40 "Movie" => Self::Movie,
41 "Series" => Self::Series,
42 "Season" => Self::Season,
43 "Episode" => Self::Episode,
44 "Video" => Self::Video,
45 "Folder" | "CollectionFolder" => Self::Folder,
46 _ => Self::Other,
47 }
48 }
49
50 pub fn is_audio(&self) -> bool {
51 matches!(self, Self::Audio | Self::MusicAlbum | Self::MusicArtist)
52 }
53
54 pub fn is_video(&self) -> bool {
55 matches!(
56 self,
57 Self::Movie | Self::Series | Self::Season | Self::Episode | Self::Video
58 )
59 }
60
61 pub fn is_playable(&self) -> bool {
62 matches!(
63 self,
64 Self::Audio | Self::Movie | Self::Episode | Self::Video
65 )
66 }
67
68 pub fn icon(&self) -> &'static str {
69 match self {
70 Self::Audio => "♪",
71 Self::MusicAlbum => "◈",
72 Self::MusicArtist => "◊",
73 Self::Playlist => "▤",
74 Self::Movie => "▶",
75 Self::Series => "▤",
76 Self::Season => "◱",
77 Self::Episode => "▶",
78 Self::Video => "▶",
79 Self::Folder => "▸",
80 Self::Other => "•",
81 }
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86#[serde(rename_all = "PascalCase")]
87pub struct BaseItem {
88 pub id: String,
89 pub name: String,
90 #[serde(rename = "Type")]
91 pub type_: String,
92 #[serde(default)]
93 pub album: Option<String>,
94 #[serde(default)]
95 pub album_id: Option<String>,
96 #[serde(default)]
97 pub album_artist: Option<String>,
98 #[serde(default)]
99 pub artists: Option<Vec<String>>,
100 #[serde(default)]
101 pub series_name: Option<String>,
102 #[serde(default)]
103 pub production_year: Option<i32>,
104 #[serde(default)]
105 pub run_time_ticks: Option<i64>,
106 #[serde(default)]
107 pub media_type: Option<String>,
108 #[serde(default)]
109 pub index_number: Option<i32>,
110 #[serde(default)]
111 pub parent_index_number: Option<i32>,
112 #[serde(default)]
113 pub image_tags: Option<serde_json::Value>,
114 #[serde(default)]
115 pub is_folder: Option<bool>,
116 #[serde(default)]
117 pub overview: Option<String>,
118}
119
120impl BaseItem {
121 pub fn kind(&self) -> ItemKind {
122 ItemKind::parse(&self.type_)
123 }
124
125 pub fn duration_secs(&self) -> Option<u64> {
126 self.run_time_ticks.map(|t| (t / 10_000_000) as u64)
127 }
128
129 pub fn subtitle(&self) -> String {
130 match self.kind() {
131 ItemKind::Audio => {
132 let artists = self
133 .artists
134 .as_ref()
135 .and_then(|a| {
136 if a.is_empty() {
137 None
138 } else {
139 Some(a.join(", "))
140 }
141 })
142 .or_else(|| self.album_artist.clone())
143 .unwrap_or_default();
144 let album = self.album.clone().unwrap_or_default();
145 match (artists.is_empty(), album.is_empty()) {
146 (true, true) => String::new(),
147 (false, true) => artists,
148 (true, false) => album,
149 (false, false) => format!("{} — {}", artists, album),
150 }
151 }
152 ItemKind::MusicAlbum => self
153 .album_artist
154 .clone()
155 .or_else(|| self.artists.as_ref().map(|a| a.join(", ")))
156 .unwrap_or_default(),
157 ItemKind::MusicArtist => String::new(),
158 ItemKind::Movie => self
159 .production_year
160 .map(|y| y.to_string())
161 .unwrap_or_default(),
162 ItemKind::Episode => match (self.parent_index_number, self.index_number) {
163 (Some(s), Some(e)) => format!(
164 "S{:02}E{:02} — {}",
165 s,
166 e,
167 self.series_name.clone().unwrap_or_default()
168 ),
169 _ => self.series_name.clone().unwrap_or_default(),
170 },
171 ItemKind::Series => self
172 .production_year
173 .map(|y| y.to_string())
174 .unwrap_or_default(),
175 _ => String::new(),
176 }
177 }
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
181#[serde(rename_all = "PascalCase")]
182pub struct SearchResult {
183 #[serde(default)]
184 pub items: Vec<BaseItem>,
185 #[serde(default)]
186 pub total_record_count: i64,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190#[serde(rename_all = "PascalCase")]
191pub struct SearchHintResult {
192 #[serde(default)]
193 pub search_hints: Vec<SearchHint>,
194 #[serde(default)]
195 pub total_record_count: i64,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
199#[serde(rename_all = "PascalCase")]
200pub struct SearchHint {
201 #[serde(default, alias = "ItemId")]
202 pub id: String,
203 pub name: String,
204 #[serde(rename = "Type", default)]
205 pub type_: String,
206 #[serde(default)]
207 pub album: Option<String>,
208 #[serde(default)]
209 pub album_artist: Option<String>,
210 #[serde(default)]
211 pub artists: Option<Vec<String>>,
212 #[serde(default)]
213 pub production_year: Option<i32>,
214 #[serde(default)]
215 pub run_time_ticks: Option<i64>,
216 #[serde(default)]
217 pub media_type: Option<String>,
218 #[serde(default)]
219 pub series_name: Option<String>,
220}
221
222impl SearchHint {
223 pub fn into_base_item(self) -> BaseItem {
224 BaseItem {
225 id: self.id,
226 name: self.name,
227 type_: self.type_,
228 album: self.album,
229 album_id: None,
230 album_artist: self.album_artist,
231 artists: self.artists,
232 series_name: self.series_name,
233 production_year: self.production_year,
234 run_time_ticks: self.run_time_ticks,
235 media_type: self.media_type,
236 index_number: None,
237 parent_index_number: None,
238 image_tags: None,
239 is_folder: None,
240 overview: None,
241 }
242 }
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
246#[serde(rename_all = "PascalCase")]
247pub struct UserView {
248 pub id: String,
249 pub name: String,
250 #[serde(default)]
251 pub collection_type: Option<String>,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
255#[serde(rename_all = "PascalCase")]
256pub struct UserViewsResult {
257 #[serde(default)]
258 pub items: Vec<UserView>,
259}
260
261pub type Playlist = BaseItem;
262pub type PlaylistItem = BaseItem;