Repository: IgnisDa/ryot (language: rust)

Issue:
[FEATURE REQUEST] Rethink monitored media and updating them
Hi there, 

Was there an update to the categories recently? Almost all of my shows lost the tag "monitoring" without action on my part, which means the "upcoming" section is now basically empty. Did something change in teh way the categories work? I redid it manually, but I just wanted to check that it was intended behaviour.

Thanks!

Edit: after putting them back on the "Monitoring" list manually and running a "Perform all background tasks", the upcoming section is still empty. I'm not sure if I missed something, is that section now reserved to "Watchlist" category instead of "Monitoring"?


Discussion / hints:
Yes items are automatically removed from monitoring after 30 days.

> upcoming section is still empty

I think they should show up after the nightly cron job. If they do not, please ping me here.
Duly noted for the 30 days, but then maybe I'm misunderstanding something about the way to stay up to date on series. 

Let's say I watch this year's season of a TV show that I'm monitoring. I finish the season, it gets the "completed" since next season isn't announced yet. The "monitoring" tag disappears after a month, and a new season is later announced. How do I make sure I actually get the show in the upcoming section when the next season is released a year late, since it wouldn't have the "monitoring" tag anymore?

Thanks!
There is an env variable: `MEDIA_MONITORING_REMOVE_AFTER_DAYS` (defaults to 30) which can be used to control this.

But you raise a valid concern which I do not have a solution for. The reason I implemented this feature was because once too many movies/shows are added to the collection, the nightly cron job sends too many requests to tmdb and gets rate limited. I fear that the keys hard-coded into ryot might be banned.

Can you think of a better approach to this?
I once implemented a solution using [collection information templates](https://ryot.io/features/#supercharged_collections) which asked users how many days they want to keep media in monitoring. But the resulting UX was bad I removed it.

Maybe some kind of exponential back-off will work here? But I have never implemented those.
That's a good point. Maybe past a certain time, monitored series could go into a sort of "cold storage" where the cron job is much less frequent? For instance: 30 days after last episode aired, cron job becomes weekly for that series? That might still mean some rate limiting if all these weekly jobs happen at the same time for all users though. 

Alternatively, I don't know if there is a way to pull only the next episode date from another source. The idea is that a proper pull from TVDB is only needed when there is actually a new season planned. Is the calendar currently working on the same system as the "monitoring"? 

As a side note, sonarr has probably figured out a way around this, since their calendar remains up to date with the upcoming episodes, with the option to show either only the shows you monitor or every show you have in your db. I'm not sure how they do it, though...
> Is the calendar currently working on the same system as the "monitoring"?

As soon as a media is first committed to database, all the events associated to it are added to calendar. When a change is detected in a media, it is refreshed and new dates are also committed to the calendar and also clean up out dated ones.

> sonarr has probably figured out a way around this

Maybe you can get a PR going? :P
> As soon as a media is first committed to database, all the events associated to it are added to calendar. When a change is detected in a media, it is refreshed and new dates are also committed to the calendar and also clean up out dated ones.

But those changes come from the same pulls as the ones for the "monitoring" category, or is it a separate one? (I'm wondering if it's possible to rely on the calendar to then trigger whether or not tvdb should be queried, or if it's just circular reasoning). 

> Maybe you can get a PR going? :P

To be absolutely honest: I'd love to, but while I sort of understand how IT works, that's not my field at all and I've litterally never coded anything in my life, I have no idea how the "behind the scenes" works. :x


> But those changes come from the same pulls as the ones for the "monitoring" category, or is it a separate one? (I'm wondering if it's possible to rely on the calendar to then trigger whether or not tvdb should be queried, or if it's just circular reasoning).

No the calendar committed to the db is not a reliable source for this purpose. The only reliable one is the TMDB API.

I think your idea of "cold storage" is pretty good. I will see what I can do.
One problem I am encountering: there is no easy way to tell if a media item has actually changed once I get it from the API. So I can't even update the `last_changed` field reliably. 🤔 idk how to solve this lol. Will have to give it more thought.
hey guys what about using status of the show, so for instance in tmdb there is a status field
![image](https://github.com/user-attachments/assets/4dd46f88-2a2b-4fdf-8e55-6cb5f870e2f6)

if that status field is 'ended' or 'canceled' as the example above, there after completing all episodes, it can be removed from monitoring because it is done (also the 30 days will work).

![image](https://github.com/user-attachments/assets/c17cd6ab-ab5b-4b1f-9b52-1e6e996bbaa1)
but is the status is other than 'ended' or 'canceled' they should continue to be monitoring even after completing all the episoded.. i do not think people will have more than 10 to 20 shows that they will be watching/waiting for a new season. 

That's a neat idea. What about media which is not from tmdb? For example books from open library. On what basis would they be removed?
well this will work for tv shows, for books i guess it should not matter as each book has its own name so i am not sure how you keep those but they are each its own. on tv shows you have the returning every year or 2 so that is what it makes it special and different
So it would have to be monitored for 2 years 😱😱😱??
lol, well i mean i guess you can add a validation that if monitoring and not cancel or ended, can be check every month or so until you actually get the returning date for the first episode of the new season.. then it will get back to the normal monitoring.. i guess it needs a new parameter that will handle if the user finished it and it is not ended or cancelled then that parameter is set to true and it will trigger the API only once a month or so until it gets back the returning date, then it will go back into the regular monitoring. and it would also go into the up next to watch
Closing this since I feel like this behaviour is fine.

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 9aff972a42..1a8029d590 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1136,9 +1136,11 @@ dependencies = [
  "async-graphql",
  "chrono",
  "common-models",
+ "common-utils",
  "database-models",
  "sea-orm",
  "sea-query",
+ "tracing",
  "uuid",
 ]
 
@@ -1781,6 +1783,7 @@ dependencies = [
  "tracing",
  "traits",
  "user-models",
+ "uuid",
 ]
 
 [[package]]
diff --git a/crates/migrations/src/lib.rs b/crates/migrations/src/lib.rs
index 5fa8625ddb..fd2da26dae 100644
--- a/crates/migrations/src/lib.rs
+++ b/crates/migrations/src/lib.rs
@@ -44,6 +44,7 @@ mod m20241004_create_application_cache;
 mod m20241006_changes_for_issue_1056;
 mod m20241010_changes_for_issue_708;
 mod m20241013_changes_for_issue_1052;
+mod m20241019_changes_for_issue_929;
 
 pub use m20230410_create_metadata::Metadata as AliasedMetadata;
 pub use m20230413_create_person::Person as AliasedPerson;
@@ -108,6 +109,7 @@ impl MigratorTrait for Migrator {
             Box::new(m20241006_changes_for_issue_1056::Migration),
             Box::new(m20241010_changes_for_issue_708::Migration),
             Box::new(m20241013_changes_for_issue_1052::Migration),
+            Box::new(m20241019_changes_for_issue_929::Migration),
         ]
     }
 }
diff --git a/crates/migrations/src/m20240904_create_monitored_entity.rs b/crates/migrations/src/m20240904_create_monitored_entity.rs
index 7b97a307ad..4f5ab3a57f 100644
--- a/crates/migrations/src/m20240904_create_monitored_entity.rs
+++ b/crates/migrations/src/m20240904_create_monitored_entity.rs
@@ -10,6 +10,7 @@ pub static MONITORED_ENTITY_VIEW_CREATION_SQL: &str = indoc! { r#"
         ute."user_id",
         cte."entity_id",
         cte."entity_lot",
+        cte."id" as "collection_to_entity_id",
         cte."collection_id" AS "origin_collection_id"
     FROM
         "collection_to_entity" cte
diff --git a/crates/migrations/src/m20241019_changes_for_issue_929.rs b/crates/migrations/src/m20241019_changes_for_issue_929.rs
new file mode 100644
index 0000000000..0544bb07a1
--- /dev/null
+++ b/crates/migrations/src/m20241019_changes_for_issue_929.rs
@@ -0,0 +1,22 @@
+use sea_orm_migration::prelude::*;
+
+use crate::m20240904_create_monitored_entity::MONITORED_ENTITY_VIEW_CREATION_SQL;
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+        let db = manager.get_connection();
+        db.execute_unprepared(r#"DROP VIEW "monitored_entity""#)
+            .await?;
+        db.execute_unprepared(MONITORED_ENTITY_VIEW_CREATION_SQL)
+            .await?;
+        Ok(())
+    }
+
+    async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
+        Ok(())
+    }
+}
diff --git a/crates/models/database/src/monitored_entity.rs b/crates/models/database/src/monitored_entity.rs
index d761d28aa1..87950570fe 100644
--- a/crates/models/database/src/monitored_entity.rs
+++ b/crates/models/database/src/monitored_entity.rs
@@ -14,6 +14,7 @@ pub struct Model {
     #[sea_orm(primary_key, auto_increment = false)]
     pub entity_lot: EntityLot,
     pub origin_collection_id: String,
+    pub collection_to_entity_id: Uuid,
 }
 
 #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
diff --git a/crates/models/database/src/seen.rs b/crates/models/database/src/seen.rs
index c5263e180f..fa79af3287 100644
--- a/crates/models/database/src/seen.rs
+++ b/crates/models/database/src/seen.rs
@@ -2,7 +2,7 @@
 
 use async_graphql::SimpleObject;
 use async_trait::async_trait;
-use chrono::NaiveDate;
+use chrono::{NaiveDate, Utc};
 use educe::Educe;
 use enums::{EntityLot, SeenState};
 use media_models::{
@@ -102,6 +102,7 @@ impl ActiveModelBehavior for ActiveModel {
         let progress = self.progress.clone().unwrap();
         if progress == dec!(100) && state == SeenState::InProgress {
             self.state = ActiveValue::Set(SeenState::Completed);
+            self.finished_on = ActiveValue::Set(Some(Utc::now().date_naive()));
         }
         if insert {
             self.id = ActiveValue::Set(format!("see_{}", nanoid!(12)));
diff --git a/crates/providers/src/mal.rs b/crates/providers/src/mal.rs
index 4869c68c3c..487f895d6d 100644
--- a/crates/providers/src/mal.rs
+++ b/crates/providers/src/mal.rs
@@ -7,8 +7,8 @@ use convert_case::{Case, Casing};
 use dependent_models::SearchResults;
 use enums::{MediaLot, MediaSource};
 use media_models::{
-    AnimeSpecifics, MangaSpecifics, MetadataDetails, MetadataImageForMediaDetails, MetadataSearchItem,
-    PartialMetadataWithoutId,
+    AnimeSpecifics, MangaSpecifics, MetadataDetails, MetadataImageForMediaDetails,
+    MetadataSearchItem, PartialMetadataWithoutId,
 };
 use rand::{seq::SliceRandom, thread_rng};
 use reqwest::{
diff --git a/crates/services/cache/Cargo.toml b/crates/services/cache/Cargo.toml
index 129a158a2b..db078fa18b 100644
--- a/crates/services/cache/Cargo.toml
+++ b/crates/services/cache/Cargo.toml
@@ -7,7 +7,12 @@ edition = "2021"
 async-graphql = { workspace = true }
 chrono = { workspace = true }
 common-models = { path = "../../models/common" }
+common-utils = { path = "../../utils/common" }
 database-models = { path = "../../models/database" }
 sea-orm = { workspace = true }
 sea-query = { workspace = true }
+tracing = { workspace = true }
 uuid = { workspace = true }
+
+[package.metadata.cargo-machete]
+ignored = ["tracing"]
diff --git a/crates/services/cache/src/lib.rs b/crates/services/cache/src/lib.rs
index ac794229fe..1614fc12e3 100644
--- a/crates/services/cache/src/lib.rs
+++ b/crates/services/cache/src/lib.rs
@@ -1,6 +1,7 @@
 use async_graphql::Result;
 use chrono::{Duration, Utc};
 use common_models::ApplicationCacheKey;
+use common_utils::ryot_log;
 use database_models::{application_cache, prelude::ApplicationCache};
 use sea_orm::{ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
 use sea_query::OnConflict;
@@ -40,7 +41,9 @@ impl CacheService {
             )
             .exec(&self.db)
             .await?;
-        Ok(inserted.last_insert_id)
+        let insert_id = inserted.last_insert_id;
+        ryot_log!(debug, "Inserted application cache with id = {insert_id:?}");
+        Ok(insert_id)
     }
 
     pub async fn get(&self, key: ApplicationCacheKey) -> Result<Option<()>> {
diff --git a/crates/services/importer/src/jellyfin.rs b/crates/services/importer/src/jellyfin.rs
index 86f0218223..d949d8b5e7 100644
--- a/crates/services/importer/src/jellyfin.rs
+++ b/crates/services/importer/src/jellyfin.rs
@@ -179,7 +179,7 @@ pub async fn import(input: DeployUrlAndKeyAndUsernameImportInput) -> Result<Impo
                 seen_history: vec![seen],
                 identifier: tmdb_id,
                 collections,
-..Default::default()
+                ..Default::default()
             });
         } else {
             failed_items.push(ImportFailedItem {
diff --git a/crates/services/integration/src/lib.rs b/crates/services/integration/src/lib.rs
index 01ffcdb7a5..5277aad9bf 100644
--- a/crates/services/integration/src/lib.rs
+++ b/crates/services/integration/src/lib.rs
@@ -57,6 +57,7 @@ impl IntegrationService {
                 None => false,
             });
             media.seen_history.iter_mut().for_each(|update| {
+                update.ended_on = Some(Utc::now().date_naive());
                 if let Some(progress) = update.progress {
                     if progress > integration.maximum_progress.unwrap() {
                         ryot_log!(
diff --git a/crates/services/integration/src/sink/emby.rs b/crates/services/integration/src/sink/emby.rs
index 7eaad2271c..cc21a57cb7 100644
--- a/crates/services/integration/src/sink/emby.rs
+++ b/crates/services/integration/src/sink/emby.rs
@@ -98,10 +98,10 @@ impl EmbyIntegration {
                 identifier,
                 source: MediaSource::Tmdb,
                 seen_history: vec![ImportOrExportMediaItemSeen {
+                    provider_watched_on: Some("Emby".to_string()),
                     progress: Some(position / runtime * dec!(100)),
                     show_season_number: payload.item.season_number,
                     show_episode_number: payload.item.episode_number,
-                    provider_watched_on: Some("Emby".to_string()),
                     ..Default::default()
                 }],
                 ..Default::default()
diff --git a/crates/services/integration/src/sink/kodi.rs b/crates/services/integration/src/sink/kodi.rs
index 2b8864dfc0..0608a8491e 100644
--- a/crates/services/integration/src/sink/kodi.rs
+++ b/crates/services/integration/src/sink/kodi.rs
@@ -35,9 +35,9 @@ impl KodiIntegration {
                 identifier: payload.identifier,
                 seen_history: vec![ImportOrExportMediaItemSeen {
                     progress: Some(payload.progress),
+                    provider_watched_on: Some("Kodi".to_string()),
                     show_season_number: payload.show_season_number,
                     show_episode_number: payload.show_episode_number,
-                    provider_watched_on: Some("Kodi".to_string()),
                     ..Default::default()
                 }],
                 ..Default::default()
diff --git a/crates/services/integration/src/yank/komga.rs b/crates/services/integration/src/yank/komga.rs
index fbcb1bf327..03ea600879 100644
--- a/crates/services/integration/src/yank/komga.rs
+++ b/crates/services/integration/src/yank/komga.rs
@@ -337,7 +337,7 @@ impl KomgaIntegration {
         }
     }
 
-    fn calculate_percentage(current_page: i32, total_page: i32) -> Decimal {
+    fn calculate_percentage(&self, current_page: i32, total_page: i32) -> Decimal {
         if total_page == 0 {
             return dec!(0);
         }
@@ -375,12 +375,11 @@ impl KomgaIntegration {
                 force_update: None,
             },
             ImportOrExportMediaItemSeen {
-                manga_chapter_number: Some(book.metadata.number.parse().unwrap_or_default()),
-                progress: Some(Self::calculate_percentage(
-                    book.read_progress.page,
-                    book.media.pages_count,
-                )),
+                progress: Some(
+                    self.calculate_percentage(book.read_progress.page, book.media.pages_count),
+                ),
                 provider_watched_on: Some("Komga".to_string()),
+                manga_chapter_number: Some(book.metadata.number.parse().unwrap_or_default()),
                 ..Default::default()
             },
         ))
@@ -422,7 +421,9 @@ impl KomgaIntegration {
             })
             .collect()
             .await;
-        result.metadata.extend(unique_collection_updates.into_values());
+        result
+            .metadata
+            .extend(unique_collection_updates.into_values());
         Ok(())
     }
 
@@ -500,8 +501,8 @@ impl KomgaIntegration {
             result.metadata.push(ImportOrExportMediaItem {
                 lot: commit.lot,
                 source: commit.source,
-                identifier: commit.identifier,
                 seen_history: vec![hist],
+                identifier: commit.identifier,
                 ..Default::default()
             });
         });
diff --git a/crates/services/miscellaneous/src/lib.rs b/crates/services/miscellaneous/src/lib.rs
index c4427e3118..96fa86fd0b 100644
--- a/crates/services/miscellaneous/src/lib.rs
+++ b/crates/services/miscellaneous/src/lib.rs
@@ -44,10 +44,11 @@ use dependent_models::{
 use dependent_utils::{
     after_media_seen_tasks, commit_metadata, commit_metadata_group_internal,
     commit_metadata_internal, commit_person, create_partial_metadata, deploy_background_job,
-    deploy_update_metadata_job, get_entities_monitored_by, get_metadata_provider,
-    get_openlibrary_service, get_tmdb_non_media_service, is_metadata_finished_by_user, post_review,
-    progress_update, queue_media_state_changed_notification_for_user,
-    queue_notifications_to_user_platforms, update_metadata_and_notify_users,
+    deploy_update_metadata_job, get_metadata_provider, get_openlibrary_service,
+    get_tmdb_non_media_service, get_users_and_cte_monitoring_entity, get_users_monitoring_entity,
+    is_metadata_finished_by_user, post_review, progress_update,
+    queue_media_state_changed_notification_for_user, queue_notifications_to_user_platforms,
+    refresh_collection_to_entity_association, update_metadata_and_notify_users,
 };
 use enums::{
     EntityLot, MediaLot, MediaSource, MetadataToMetadataRelation, SeenState, UserToMediaReason,
@@ -1992,7 +1993,7 @@ ORDER BY RANDOM() LIMIT 10;
         Ok(monitored_by)
     }
 
-    async fn update_watchlist_metadata_and_queue_notifications(&self) -> Result<()> {
+    async fn update_monitored_metadata_and_queue_notifications(&self) -> Result<()> {
         let meta_map = self.get_monitored_entities(EntityLot::Metadata).await?;
         ryot_log!(
             debug,
@@ -2650,7 +2651,7 @@ ORDER BY RANDOM() LIMIT 10;
             .collect_vec();
         for (metadata_id, notification) in notifications.into_iter() {
             let users_to_notify =
-                get_entities_monitored_by(&metadata_id, EntityLot::Metadata, &self.0.db).await?;
+                get_users_monitoring_entity(&metadata_id, EntityLot::Metadata, &self.0.db).await?;
             for user in users_to_notify {
                 queue_media_state_changed_notification_for_user(&user, &notification, &self.0)
                     .await?;
@@ -2749,9 +2750,10 @@ ORDER BY RANDOM() LIMIT 10;
             .unwrap_or_default();
         if !notifications.is_empty() {
             let users_to_notify =
-                get_entities_monitored_by(&person_id, EntityLot::Person, &self.0.db).await?;
+                get_users_and_cte_monitoring_entity(&person_id, EntityLot::Person, &self.0.db)
+                    .await?;
             for notification in notifications {
-                for user_id in users_to_notify.iter() {
+                for (user_id, cte_id) in users_to_notify.iter() {
                     queue_media_state_changed_notification_for_user(
                         user_id,
                         &notification,
@@ -2759,6 +2761,9 @@ ORDER BY RANDOM() LIMIT 10;
                     )
                     .await
                     .trace_ok();
+                    refresh_collection_to_entity_association(cte_id, &self.0.db)
+                        .await
+                        .trace_ok();
                 }
             }
         }
@@ -2782,7 +2787,7 @@ ORDER BY RANDOM() LIMIT 10;
 
     pub async fn handle_review_posted_event(&self, event: ReviewPostedEvent) -> Result<()> {
         let monitored_by =
-            get_entities_monitored_by(&event.obj_id, event.entity_lot, &self.0.db).await?;
+            get_users_monitoring_entity(&event.obj_id, event.entity_lot, &self.0.db).await?;
         let users = User::find()
             .select_only()
             .column(user::Column::Id)
@@ -2856,23 +2861,21 @@ ORDER BY RANDOM() LIMIT 10;
         struct CustomQueryResponse {
             id: Uuid,
             created_on: DateTimeUtc,
-            last_updated_on: Option<DateTimeUtc>,
+            last_updated_on: DateTimeUtc,
         }
         let all_cte = CollectionToEntity::find()
             .select_only()
             .column(collection_to_entity::Column::Id)
             .column(collection_to_entity::Column::CreatedOn)
-            .column(metadata::Column::LastUpdatedOn)
-            .left_join(Metadata)
+            .column(collection_to_entity::Column::LastUpdatedOn)
             .inner_join(Collection)
             .filter(collection::Column::Name.eq(DefaultCollection::Monitoring.to_string()))
-            .order_by_asc(collection_to_entity::Column::Id)
             .into_model::<CustomQueryResponse>()
             .all(&self.0.db)
             .await?;
         let mut to_delete = vec![];
         for cte in all_cte {
-            let delta = cte.last_updated_on.unwrap_or_else(Utc::now) - cte.created_on;
+            let delta = cte.last_updated_on - cte.created_on;
             if delta.num_days().abs() > self.0.config.media.monitoring_remove_after_days {
                 to_delete.push(cte.id);
             }
@@ -3059,18 +3062,18 @@ ORDER BY RANDOM() LIMIT 10;
 
         ryot_log!(trace, "Invalidating invalid media import jobs");
         self.invalidate_import_jobs().await.trace_ok();
-        ryot_log!(trace, "Removing stale entities from Monitoring collection");
-        self.remove_old_entities_from_monitoring_collection()
-            .await
-            .trace_ok();
         ryot_log!(trace, "Checking for updates for media in Watchlist");
-        self.update_watchlist_metadata_and_queue_notifications()
+        self.update_monitored_metadata_and_queue_notifications()
             .await
             .trace_ok();
         ryot_log!(trace, "Checking for updates for monitored people");
         self.update_monitored_people_and_queue_notifications()
             .await
             .trace_ok();
+        ryot_log!(trace, "Removing stale entities from Monitoring collection");
+        self.remove_old_entities_from_monitoring_collection()
+            .await
+            .trace_ok();
         ryot_log!(trace, "Checking and queuing any pending reminders");
         self.queue_pending_reminders().await.trace_ok();
         ryot_log!(trace, "Recalculating calendar events");
diff --git a/crates/utils/dependent/Cargo.toml b/crates/utils/dependent/Cargo.toml
index 0a5d3858ea..8b23cc7585 100644
--- a/crates/utils/dependent/Cargo.toml
+++ b/crates/utils/dependent/Cargo.toml
@@ -31,6 +31,7 @@ supporting-service = { path = "../../services/supporting" }
 tracing = { workspace = true }
 traits = { path = "../../traits" }
 user-models = { path = "../../models/user" }
+uuid = { workspace = true }
 
 [package.metadata.cargo-machete]
 ignored = ["tracing"]
diff --git a/crates/utils/dependent/src/lib.rs b/crates/utils/dependent/src/lib.rs
index ae506ff1af..86fc501dc5 100644
--- a/crates/utils/dependent/src/lib.rs
+++ b/crates/utils/dependent/src/lib.rs
@@ -12,11 +12,11 @@ use common_models::{
 };
 use common_utils::{ryot_log, SHOW_SPECIAL_SEASON_NAMES};
 use database_models::{
-    genre, metadata, metadata_group, metadata_to_genre, metadata_to_metadata, metadata_to_person,
-    monitored_entity, person,
+    collection_to_entity, genre, metadata, metadata_group, metadata_to_genre, metadata_to_metadata,
+    metadata_to_person, monitored_entity, person,
     prelude::{
-        Collection, Exercise, Genre, Metadata, MetadataGroup, MetadataToGenre, MetadataToMetadata,
-        MetadataToPerson, MonitoredEntity, Person, Seen, UserToEntity, Workout,
+        Collection, CollectionToEntity, Exercise, Genre, Metadata, MetadataGroup, MetadataToGenre,
+        MetadataToMetadata, MetadataToPerson, MonitoredEntity, Person, Seen, UserToEntity, Workout,
     },
     queued_notification, review, seen, user_measurement, user_to_entity, workout,
 };
@@ -72,6 +72,7 @@ use serde::{Deserialize, Serialize};
 use supporting_service::SupportingService;
 use traits::{MediaProvider, TraceOk};
 use user_models::{UserPreferences, UserReviewScale};
+use uuid::Uuid;
 
 pub type Provider = Box<(dyn MediaProvider + Send + Sync)>;
 
@@ -663,22 +664,37 @@ pub async fn update_metadata(
     Ok(notifications)
 }
 
-pub async fn get_entities_monitored_by(
+pub async fn get_users_and_cte_monitoring_entity(
     entity_id: &String,
     entity_lot: EntityLot,
     db: &DatabaseConnection,
-) -> Result<Vec<String>> {
+) -> Result<Vec<(String, Uuid)>> {
     let all_entities = MonitoredEntity::find()
         .select_only()
+        .column(monitored_entity::Column::CollectionToEntityId)
         .column(monitored_entity::Column::UserId)
         .filter(monitored_entity::Column::EntityId.eq(entity_id))
         .filter(monitored_entity::Column::EntityLot.eq(entity_lot))
-        .into_tuple::<String>()
+        .into_tuple::<(String, Uuid)>()
         .all(db)
         .await?;
     Ok(all_entities)
 }
 
+pub async fn get_users_monitoring_entity(
+    entity_id: &String,
+    entity_lot: EntityLot,
+    db: &DatabaseConnection,
+) -> Result<Vec<String>> {
+    Ok(
+        get_users_and_cte_monitoring_entity(entity_id, entity_lot, db)
+            .await?
+            .into_iter()
+            .map(|(u, _)| u)
+            .collect_vec(),
+    )
+}
+
 pub async fn queue_notifications_to_user_platforms(
     user_id: &String,
     msg: &str,
@@ -718,6 +734,25 @@ pub async fn queue_media_state_changed_notification_for_user(
     Ok(())
 }
 
+pub async fn refresh_collection_to_entity_association(
+    cte_id: &Uuid,
+    db: &DatabaseConnection,
+) -> Result<()> {
+    ryot_log!(
+        debug,
+        "Refreshing collection to entity association for id = {cte_id}"
+    );
+    CollectionToEntity::update_many()
+        .col_expr(
+            collection_to_entity::Column::LastUpdatedOn,
+            Expr::value(Utc::now()),
+        )
+        .filter(collection_to_entity::Column::Id.eq(cte_id.to_owned()))
+        .exec(db)
+        .await?;
+    Ok(())
+}
+
 pub async fn update_metadata_and_notify_users(
     metadata_id: &String,
     force_update: bool,
@@ -728,12 +763,15 @@ pub async fn update_metadata_and_notify_users(
         .unwrap();
     if !notifications.is_empty() {
         let users_to_notify =
-            get_entities_monitored_by(metadata_id, EntityLot::Metadata, &ss.db).await?;
+            get_users_and_cte_monitoring_entity(metadata_id, EntityLot::Metadata, &ss.db).await?;
         for notification in notifications {
-            for user_id in users_to_notify.iter() {
+            for (user_id, cte_id) in users_to_notify.iter() {
                 queue_media_state_changed_notification_for_user(user_id, &notification, ss)
                     .await
                     .trace_ok();
+                refresh_collection_to_entity_association(cte_id, &ss.db)
+                    .await
+                    .trace_ok();
             }
         }
     }
@@ -1344,9 +1382,6 @@ pub async fn progress_update(
             last_seen.updated_at = ActiveValue::Set(updated_at);
             last_seen.provider_watched_on =
                 ActiveValue::Set(input.provider_watched_on.or(watched_on));
-            if progress == dec!(100) {
-                last_seen.finished_on = ActiveValue::Set(Some(now.date_naive()));
-            }
 
             // This is needed for manga as some of the apps will update in weird orders
             // For example with komga mihon will update out of order to the server


Test patch: