Skip to content

Commit 3e0fca8

Browse files
committed
Canonicalize watched ids and migrate history
1 parent 58f7b30 commit 3e0fca8

7 files changed

Lines changed: 506 additions & 84 deletions

File tree

src/model/episodes.rs

Lines changed: 80 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
use std::collections::HashMap;
1+
use std::collections::{HashMap, HashSet};
22

33
use async_recursion::async_recursion;
44
use nanoid::nanoid;
55
use query_external_ip::SourceError;
66
use rs_plugin_common_interfaces::{
7-
domain::rs_ids::RsIds,
7+
domain::rs_ids::{ApplyRsIds, RsIds},
88
lookup::{RsLookupEpisode, RsLookupMetadataResult, RsLookupQuery},
99
ImageType, MediaType,
1010
};
@@ -32,6 +32,7 @@ use crate::{
3232
use super::{
3333
entity_search::merge_result_ids,
3434
error::{Error, Result},
35+
history::{episode_history_id, history_id_rsids},
3536
medias::{RsSort, RsSortOrder},
3637
store::sql::SqlOrder,
3738
users::{ConnectedUser, HistoryQuery},
@@ -106,6 +107,22 @@ pub struct EpisodeForUpdate {
106107
}
107108

108109
impl ModelController {
110+
async fn episode_series_by_ref(
111+
&self,
112+
library_id: &str,
113+
episodes: &[Episode],
114+
) -> RsResult<HashMap<String, Serie>> {
115+
let store = self.store.get_library_store(library_id)?;
116+
let serie_refs: HashSet<String> = episodes.iter().map(|episode| episode.serie.clone()).collect();
117+
let mut series = HashMap::new();
118+
for serie_ref in serie_refs {
119+
if let Some(serie) = store.get_serie(&serie_ref).await? {
120+
series.insert(serie_ref, serie.item);
121+
}
122+
}
123+
Ok(series)
124+
}
125+
109126
async fn lookup_episodes_metadata(
110127
&self,
111128
library_id: &str,
@@ -245,27 +262,35 @@ impl ModelController {
245262
requesting_user: &ConnectedUser,
246263
library_id: Option<String>,
247264
) -> RsResult<()> {
248-
let ids: RsIds = episode.clone().into();
249-
let watched = self
250-
.get_watched(
251-
HistoryQuery {
252-
types: vec![MediaType::Episode],
253-
id: Some(ids.clone()),
254-
..Default::default()
255-
},
256-
requesting_user,
257-
library_id.clone(),
258-
)
259-
.await?;
260-
let progress = self
261-
.get_view_progress(ids, requesting_user, library_id)
262-
.await?;
263-
if let Some(progress) = progress {
264-
episode.progress = Some(progress.progress);
265-
}
266-
let watched = watched.first();
267-
if let Some(watched) = watched {
268-
episode.watched = Some(watched.date);
265+
if let Some(library_id) = library_id {
266+
let store = self.store.get_library_store(&library_id)?;
267+
if let Some(serie) = store.get_serie(&episode.serie).await? {
268+
let history_id = episode_history_id(&serie.item, episode);
269+
let watched = self
270+
.get_watched(
271+
HistoryQuery {
272+
types: vec![MediaType::Episode],
273+
id: Some(history_id_rsids(history_id.clone())),
274+
..Default::default()
275+
},
276+
requesting_user,
277+
Some(library_id.clone()),
278+
)
279+
.await?;
280+
let progress = self
281+
.get_view_progress(
282+
history_id_rsids(history_id),
283+
requesting_user,
284+
Some(library_id),
285+
)
286+
.await?;
287+
if let Some(progress) = progress {
288+
episode.progress = Some(progress.progress);
289+
}
290+
if let Some(watched) = watched.first() {
291+
episode.watched = Some(watched.date);
292+
}
293+
}
269294
}
270295
episode.fill_imdb_ratings(&self.imdb).await;
271296
Ok(())
@@ -276,6 +301,11 @@ impl ModelController {
276301
requesting_user: &ConnectedUser,
277302
library_id: Option<String>,
278303
) -> RsResult<()> {
304+
let series_by_ref = if let Some(library_id) = library_id.as_deref() {
305+
self.episode_series_by_ref(library_id, episodes).await?
306+
} else {
307+
HashMap::new()
308+
};
279309
let watched = self
280310
.get_watched(
281311
HistoryQuery {
@@ -304,15 +334,12 @@ impl ModelController {
304334
.collect::<HashMap<_, _>>();
305335

306336
for episode in episodes {
307-
let ids = RsIds::from(episode.clone());
308-
let ids_string: Vec<String> = ids.into();
309-
for id in ids_string {
310-
let watch = watched.get(&id);
311-
if let Some(watch) = watch {
337+
if let Some(serie) = series_by_ref.get(&episode.serie) {
338+
let history_id = episode_history_id(serie, episode);
339+
if let Some(watch) = watched.get(&history_id) {
312340
episode.watched = Some(*watch);
313341
}
314-
let progress = progresses.get(&id);
315-
if let Some(progress) = progress {
342+
if let Some(progress) = progresses.get(&history_id) {
316343
episode.progress = Some(*progress);
317344
}
318345
}
@@ -520,6 +547,20 @@ impl ModelController {
520547
let ids = self
521548
.get_serie_ids(library_id, serie_id, requesting_user)
522549
.await?;
550+
let existing_episodes = self
551+
.get_episodes(
552+
library_id,
553+
EpisodeQuery {
554+
serie_ref: Some(serie_id.to_string()),
555+
..Default::default()
556+
},
557+
requesting_user,
558+
)
559+
.await?;
560+
let existing_ids_by_episode: HashMap<(u32, u32), RsIds> = existing_episodes
561+
.into_iter()
562+
.map(|episode| ((episode.season, episode.number), RsIds::from(episode)))
563+
.collect();
523564
let mut all_episodes = self
524565
.lookup_episodes_metadata(library_id, serie_id, &ids, requesting_user)
525566
.await?;
@@ -530,6 +571,15 @@ impl ModelController {
530571
)))
531572
.into());
532573
}
574+
for episode in &mut all_episodes {
575+
if let Some(existing_ids) =
576+
existing_ids_by_episode.get(&(episode.season, episode.number))
577+
{
578+
let mut merged_ids = RsIds::from(episode.clone());
579+
merged_ids.merge(existing_ids);
580+
episode.apply_rs_ids(&merged_ids);
581+
}
582+
}
533583
let store = self.store.get_library_store_optional(library_id).ok_or(
534584
Error::LibraryStoreNotFoundFor(
535585
library_id.clone().to_string(),

src/model/history.rs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
use std::collections::{HashMap, HashSet};
2+
3+
use rs_plugin_common_interfaces::{domain::rs_ids::RsIds, MediaType};
4+
5+
use crate::domain::{episode::Episode, library::LibraryType, movie::Movie, serie::Serie};
6+
use crate::tools::log::{log_info, LogServiceType};
7+
8+
use super::{
9+
episodes::EpisodeQuery,
10+
movies::MovieQuery,
11+
series::SerieQuery,
12+
store::sql::users::{HistoryIdRewrite, ProgressIdRewrite},
13+
ModelController,
14+
};
15+
16+
fn series_history_key(serie: &Serie) -> String {
17+
if let Some(imdb) = &serie.imdb {
18+
format!("imdb/{imdb}")
19+
} else if let Some(tmdb) = serie.tmdb {
20+
format!("tmdb/{tmdb}")
21+
} else if let Some(tvdb) = serie.tvdb {
22+
format!("tvdb/{tvdb}")
23+
} else {
24+
format!("redseat/{}", serie.id)
25+
}
26+
}
27+
28+
pub fn series_history_id(serie: &Serie) -> String {
29+
format!("series:{}", series_history_key(serie))
30+
}
31+
32+
pub fn movie_history_id(movie: &Movie) -> String {
33+
if let Some(imdb) = &movie.imdb {
34+
format!("movie:imdb/{imdb}")
35+
} else if let Some(tmdb) = movie.tmdb {
36+
format!("movie:tmdb/{tmdb}")
37+
} else {
38+
format!("movie:redseat/{}", movie.id)
39+
}
40+
}
41+
42+
pub fn episode_history_id(serie: &Serie, episode: &Episode) -> String {
43+
format!(
44+
"episode:{}/{}/{}",
45+
series_history_key(serie),
46+
episode.season,
47+
episode.number
48+
)
49+
}
50+
51+
pub fn history_id_rsids(id: String) -> RsIds {
52+
RsIds::try_from(id).expect("history ids must stay in key:value format")
53+
}
54+
55+
fn dedup_ids(ids: Vec<String>) -> Vec<String> {
56+
let mut seen = HashSet::new();
57+
ids.into_iter().filter(|id| seen.insert(id.clone())).collect()
58+
}
59+
60+
fn movie_legacy_ids(movie: &Movie) -> Vec<String> {
61+
let ids: Vec<String> = RsIds::from(movie.clone()).into();
62+
dedup_ids(ids)
63+
}
64+
65+
fn episode_legacy_ids(episode: &Episode) -> Vec<String> {
66+
let ids: Vec<String> = RsIds::from(episode.clone()).into();
67+
dedup_ids(ids)
68+
}
69+
70+
impl ModelController {
71+
pub async fn migrate_history_ids(&self) -> crate::Result<()> {
72+
let watched = self.store.get_all_watched().await?;
73+
let progress = self.store.get_all_view_progress_rows().await?;
74+
let has_history = watched
75+
.iter()
76+
.any(|row| matches!(row.kind, MediaType::Movie | MediaType::Episode))
77+
|| progress
78+
.iter()
79+
.any(|row| matches!(row.kind, MediaType::Movie | MediaType::Episode));
80+
if !has_history {
81+
return Ok(());
82+
}
83+
84+
let libraries = self.store.get_libraries().await?;
85+
let mut movie_targets = HashMap::<String, String>::new();
86+
let mut episode_targets = HashMap::<String, (String, String)>::new();
87+
88+
for library in libraries {
89+
if !matches!(library.kind, LibraryType::Movies | LibraryType::Shows) {
90+
continue;
91+
}
92+
let store = self.store.get_library_store(&library.id)?;
93+
94+
for movie in store.get_movies(MovieQuery::default()).await? {
95+
let canonical_id = movie_history_id(&movie);
96+
for legacy_id in movie_legacy_ids(&movie) {
97+
if legacy_id != canonical_id {
98+
movie_targets.entry(legacy_id).or_insert_with(|| canonical_id.clone());
99+
}
100+
}
101+
}
102+
103+
let series = store.get_series(SerieQuery::default()).await?;
104+
let series_by_ref: HashMap<String, Serie> = series
105+
.into_iter()
106+
.map(|serie| (serie.item.id.clone(), serie.item))
107+
.collect();
108+
for episode in store.get_episodes(EpisodeQuery::default()).await? {
109+
let Some(serie) = series_by_ref.get(&episode.serie) else {
110+
continue;
111+
};
112+
let canonical_id = episode_history_id(serie, &episode);
113+
let parent_id = series_history_id(serie);
114+
for legacy_id in episode_legacy_ids(&episode) {
115+
if legacy_id != canonical_id {
116+
episode_targets
117+
.entry(legacy_id)
118+
.or_insert_with(|| (canonical_id.clone(), parent_id.clone()));
119+
}
120+
}
121+
}
122+
}
123+
124+
let watched_rewrites = watched
125+
.into_iter()
126+
.filter_map(|watched| match watched.kind {
127+
MediaType::Movie => movie_targets.get(&watched.id).map(|new_id| HistoryIdRewrite {
128+
kind: watched.kind,
129+
old_id: watched.id,
130+
new_id: new_id.clone(),
131+
user_ref: watched.user_ref.unwrap_or_default(),
132+
}),
133+
MediaType::Episode => {
134+
episode_targets
135+
.get(&watched.id)
136+
.map(|(new_id, _)| HistoryIdRewrite {
137+
kind: watched.kind,
138+
old_id: watched.id,
139+
new_id: new_id.clone(),
140+
user_ref: watched.user_ref.unwrap_or_default(),
141+
})
142+
}
143+
_ => None,
144+
})
145+
.collect::<Vec<_>>();
146+
let progress_rewrites = progress
147+
.into_iter()
148+
.filter_map(|progress| match progress.kind {
149+
MediaType::Movie => movie_targets.get(&progress.id).map(|new_id| ProgressIdRewrite {
150+
kind: progress.kind,
151+
old_id: progress.id,
152+
new_id: new_id.clone(),
153+
new_parent: None,
154+
user_ref: progress.user_ref,
155+
}),
156+
MediaType::Episode => {
157+
episode_targets
158+
.get(&progress.id)
159+
.map(|(new_id, parent_id)| ProgressIdRewrite {
160+
kind: progress.kind,
161+
old_id: progress.id,
162+
new_id: new_id.clone(),
163+
new_parent: Some(parent_id.clone()),
164+
user_ref: progress.user_ref,
165+
})
166+
}
167+
_ => None,
168+
})
169+
.collect::<Vec<_>>();
170+
171+
let (watched_count, progress_count) = self
172+
.store
173+
.apply_history_rewrites(watched_rewrites, progress_rewrites)
174+
.await?;
175+
log_info(
176+
LogServiceType::Database,
177+
format!(
178+
"History migration complete: watched rewrites={}, progress rewrites={}",
179+
watched_count, progress_count
180+
),
181+
);
182+
Ok(())
183+
}
184+
}

src/model/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub mod deleted;
1313
pub mod entity_images;
1414
pub mod entity_search;
1515
pub mod episodes;
16+
pub mod history;
1617
pub mod media_progresses;
1718
pub mod media_ratings;
1819
pub mod medias;
@@ -188,6 +189,7 @@ impl ModelController {
188189
});
189190

190191
mc.cache_update_all_libraries().await?;
192+
mc.migrate_history_ids().await?;
191193

192194
let scheduler = &mc.scheduler;
193195
scheduler.start(mc.clone()).await?;

0 commit comments

Comments
 (0)