Repository: IgnisDa/ryot (language: rust)

Issue:
Import from Anilist



Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index a01137553a..3b599bcd84 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -810,7 +810,7 @@ dependencies = [
  "async-graphql-axum",
  "aws-sdk-s3",
  "axum",
- "background",
+ "background-models",
  "cache-service",
  "chrono-tz",
  "collection-resolver",
@@ -833,7 +833,6 @@ dependencies = [
  "integration-service",
  "itertools 0.13.0",
  "logs-wheel",
- "media-models",
  "migrations",
  "miscellaneous-resolver",
  "miscellaneous-service",
@@ -848,7 +847,6 @@ dependencies = [
  "statistics-service",
  "supporting-service",
  "tokio",
- "tower",
  "tower-http",
  "tracing",
  "tracing-subscriber",
@@ -858,15 +856,12 @@ dependencies = [
 ]
 
 [[package]]
-name = "background"
+name = "background-models"
 version = "0.1.0"
 dependencies = [
- "apalis",
  "chrono",
  "chrono-tz",
  "database-models",
- "enums",
- "fitness-models",
  "media-models",
  "serde",
  "strum",
@@ -1293,7 +1288,7 @@ dependencies = [
  "database-utils",
  "dependent-models",
  "dependent-utils",
- "enums",
+ "enum-models",
  "media-models",
  "migrations",
  "sea-orm",
@@ -1327,8 +1322,8 @@ dependencies = [
  "async-graphql",
  "chrono",
  "educe",
+ "enum-models",
  "enum_meta",
- "enums",
  "rust_decimal",
  "schematic",
  "sea-orm",
@@ -1345,7 +1340,7 @@ dependencies = [
  "chrono",
  "compile-time",
  "const-str",
- "enums",
+ "enum-models",
  "env-utils",
  "reqwest 0.12.9",
  "serde",
@@ -1680,7 +1675,7 @@ dependencies = [
  "chrono",
  "common-models",
  "educe",
- "enums",
+ "enum-models",
  "file-storage-service",
  "fitness-models",
  "importer-models",
@@ -1703,13 +1698,13 @@ version = "0.1.0"
 dependencies = [
  "application-utils",
  "async-graphql",
- "background",
+ "background-models",
  "chrono",
  "common-models",
  "common-utils",
  "database-models",
  "dependent-models",
- "enums",
+ "enum-models",
  "fitness-models",
  "futures",
  "itertools 0.13.0",
@@ -1736,7 +1731,7 @@ dependencies = [
  "common-models",
  "config",
  "database-models",
- "enums",
+ "enum-models",
  "fitness-models",
  "importer-models",
  "media-models",
@@ -1755,7 +1750,7 @@ version = "0.1.0"
 dependencies = [
  "application-utils",
  "async-graphql",
- "background",
+ "background-models",
  "chrono",
  "common-models",
  "common-utils",
@@ -1763,7 +1758,7 @@ dependencies = [
  "database-models",
  "database-utils",
  "dependent-models",
- "enums",
+ "enum-models",
  "file-storage-service",
  "fitness-models",
  "importer-models",
@@ -2119,6 +2114,19 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "enum-models"
+version = "0.1.0"
+dependencies = [
+ "async-graphql",
+ "schematic",
+ "sea-orm",
+ "sea-orm-migration",
+ "serde",
+ "serde_json",
+ "strum",
+]
+
 [[package]]
 name = "enum-ordinalize"
 version = "4.3.0"
@@ -2145,19 +2153,6 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8ea6bd8eac0d352f66b6ba30dbb38bb0f68410d5aa538eaff17fa791b97279ae"
 
-[[package]]
-name = "enums"
-version = "0.1.0"
-dependencies = [
- "async-graphql",
- "schematic",
- "sea-orm",
- "sea-orm-migration",
- "serde",
- "serde_json",
- "strum",
-]
-
 [[package]]
 name = "env-utils"
 version = "0.1.0"
@@ -2229,14 +2224,14 @@ name = "exporter-service"
 version = "0.1.0"
 dependencies = [
  "async-graphql",
- "background",
+ "background-models",
  "chrono",
  "common-models",
  "common-utils",
  "database-models",
  "database-utils",
  "dependent-models",
- "enums",
+ "enum-models",
  "fitness-models",
  "itertools 0.13.0",
  "media-models",
@@ -2342,7 +2337,7 @@ dependencies = [
  "common-models",
  "derive_more 1.0.0",
  "educe",
- "enums",
+ "enum-models",
  "rust_decimal",
  "schematic",
  "sea-orm",
@@ -2371,7 +2366,7 @@ version = "0.1.0"
 dependencies = [
  "application-utils",
  "async-graphql",
- "background",
+ "background-models",
  "common-models",
  "common-utils",
  "const-str",
@@ -2379,7 +2374,7 @@ dependencies = [
  "database-utils",
  "dependent-models",
  "dependent-utils",
- "enums",
+ "enum-models",
  "fitness-models",
  "migrations",
  "nanoid",
@@ -3287,7 +3282,7 @@ name = "importer-models"
 version = "0.1.0"
 dependencies = [
  "async-graphql",
- "enums",
+ "enum-models",
  "sea-orm",
  "serde",
  "serde_json",
@@ -3311,7 +3306,7 @@ dependencies = [
  "anyhow",
  "application-utils",
  "async-graphql",
- "background",
+ "background-models",
  "chrono",
  "chrono-tz",
  "common-models",
@@ -3322,8 +3317,8 @@ dependencies = [
  "database-models",
  "dependent-models",
  "dependent-utils",
+ "enum-models",
  "enum_meta",
- "enums",
  "env-utils",
  "external-utils",
  "fitness-models",
@@ -3343,6 +3338,7 @@ dependencies = [
  "serde_json",
  "serde_with 3.11.0",
  "specific-models",
+ "specific-utils",
  "supporting-service",
  "tracing",
  "traits",
@@ -3401,7 +3397,7 @@ dependencies = [
  "database-utils",
  "dependent-models",
  "dependent-utils",
- "enums",
+ "enum-models",
  "eventsource-stream",
  "external-utils",
  "itertools 0.13.0",
@@ -3418,6 +3414,7 @@ dependencies = [
  "serde_json",
  "sonarr-api-rs",
  "specific-models",
+ "specific-utils",
  "supporting-service",
  "tokio",
  "tracing",
@@ -3717,7 +3714,7 @@ dependencies = [
  "chrono",
  "common-models",
  "common-utils",
- "enums",
+ "enum-models",
  "rust_decimal",
  "schematic",
  "sea-orm",
@@ -3759,7 +3756,7 @@ dependencies = [
 name = "migrations"
 version = "0.1.0"
 dependencies = [
- "enums",
+ "enum-models",
  "indoc",
  "sea-orm",
  "sea-orm-migration",
@@ -3826,7 +3823,7 @@ version = "0.1.0"
 dependencies = [
  "application-utils",
  "async-graphql",
- "background",
+ "background-models",
  "chrono",
  "common-models",
  "common-utils",
@@ -3834,7 +3831,7 @@ dependencies = [
  "database-utils",
  "dependent-models",
  "dependent-utils",
- "enums",
+ "enum-models",
  "futures",
  "itertools 0.13.0",
  "markdown",
@@ -4605,7 +4602,7 @@ dependencies = [
  "database-models",
  "dependent-models",
  "educe",
- "enums",
+ "enum-models",
  "graphql_client",
  "hashbag",
  "itertools 0.13.0",
@@ -5886,6 +5883,19 @@ dependencies = [
  "strum",
 ]
 
+[[package]]
+name = "specific-utils"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "database-models",
+ "dependent-utils",
+ "enum-models",
+ "sea-orm",
+ "supporting-service",
+ "tokio",
+]
+
 [[package]]
 name = "spin"
 version = "0.9.8"
@@ -6193,7 +6203,7 @@ dependencies = [
  "database-models",
  "database-utils",
  "dependent-models",
- "enums",
+ "enum-models",
  "hashbag",
  "itertools 0.13.0",
  "sea-orm",
@@ -6291,7 +6301,7 @@ version = "0.1.0"
 dependencies = [
  "apalis",
  "async-graphql",
- "background",
+ "background-models",
  "cache-service",
  "chrono",
  "chrono-tz",
@@ -6300,7 +6310,7 @@ dependencies = [
  "config",
  "database-models",
  "dependent-models",
- "enums",
+ "enum-models",
  "env-utils",
  "file-storage-service",
  "isolang",
@@ -6310,7 +6320,6 @@ dependencies = [
  "sea-orm",
  "serde",
  "serde_json",
- "serde_with 3.11.0",
  "tracing",
  "unkey",
 ]
@@ -7000,7 +7009,7 @@ dependencies = [
  "async-graphql",
  "common-models",
  "educe",
- "enums",
+ "enum-models",
  "fitness-models",
  "sea-orm",
  "serde",
@@ -7036,8 +7045,9 @@ dependencies = [
  "database-models",
  "database-utils",
  "dependent-models",
+ "dependent-utils",
+ "enum-models",
  "enum_meta",
- "enums",
  "fitness-models",
  "itertools 0.13.0",
  "jwt-service",
diff --git a/Cargo.toml b/Cargo.toml
index 6a3c6ec945..e9361de94d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,13 +1,13 @@
 [workspace]
 members = [
     "apps/backend",
-    "crates/background",
     "crates/config",
-    "crates/enums",
     "crates/migrations",
+    "crates/models/background",
     "crates/models/common",
     "crates/models/database",
     "crates/models/dependent",
+    "crates/models/enum",
     "crates/models/fitness",
     "crates/models/importer",
     "crates/models/media",
@@ -43,13 +43,14 @@ members = [
     "crates/utils/dependent",
     "crates/utils/external",
     "crates/utils/env",
+    "crates/utils/specific",
 ]
 resolver = "2"
 
 [workspace.dependencies]
 askama = "0.12.1"
 anyhow = "=1.0.93"
-apalis = { version = "=0.6.2", features = ["limit"] }
+apalis = { version = "=0.6.2", features = ["catch-panic", "limit", "retry"] }
 apalis-cron = "=0.6.2"
 argon2 = "=0.6.0-pre.1"
 async-graphql = { version = "=7.0.11", features = [
diff --git a/apps/backend/Cargo.toml b/apps/backend/Cargo.toml
index c7b0d42357..9681a35dbc 100644
--- a/apps/backend/Cargo.toml
+++ b/apps/backend/Cargo.toml
@@ -14,7 +14,7 @@ async-graphql = { workspace = true }
 async-graphql-axum = { workspace = true }
 axum = { workspace = true }
 aws-sdk-s3 = { workspace = true }
-background = { path = "../../crates/background" }
+background-models = { path = "../../crates/models/background" }
 cache-service = { path = "../../crates/services/cache" }
 chrono-tz = { workspace = true }
 collection-resolver = { path = "../../crates/resolvers/collection" }
@@ -37,7 +37,6 @@ importer-service = { path = "../../crates/services/importer" }
 integration-service = { path = "../../crates/services/integration" }
 itertools = { workspace = true }
 logs-wheel = { workspace = true }
-media-models = { path = "../../crates/models/media" }
 migrations = { path = "../../crates/migrations" }
 miscellaneous-resolver = { path = "../../crates/resolvers/miscellaneous" }
 miscellaneous-service = { path = "../../crates/services/miscellaneous" }
@@ -52,7 +51,6 @@ statistics-resolver = { path = "../../crates/resolvers/statistics" }
 statistics-service = { path = "../../crates/services/statistics" }
 supporting-service = { path = "../../crates/services/supporting" }
 tokio = { workspace = true }
-tower = { workspace = true }
 tower-http = { workspace = true }
 tracing = { workspace = true }
 tracing-subscriber = { workspace = true }
diff --git a/apps/backend/src/common.rs b/apps/backend/src/common.rs
index 03fd996ebd..cf9534b7d6 100644
--- a/apps/backend/src/common.rs
+++ b/apps/backend/src/common.rs
@@ -10,7 +10,10 @@ use axum::{
     routing::{get, post, Router},
     Extension,
 };
-use background::{ApplicationJob, CoreApplicationJob};
+use background_models::{
+    ApplicationJob, HpApplicationJob, LpApplicationJob,
+    MpApplicationJob,
+};
 use cache_service::CacheService;
 use collection_resolver::{CollectionMutation, CollectionQuery};
 use collection_service::CollectionService;
@@ -21,6 +24,7 @@ use file_storage_resolver::{FileStorageMutation, FileStorageQuery};
 use file_storage_service::FileStorageService;
 use fitness_resolver::{FitnessMutation, FitnessQuery};
 use fitness_service::FitnessService;
+use futures::future::join_all;
 use importer_resolver::{ImporterMutation, ImporterQuery};
 use importer_service::ImporterService;
 use integration_service::IntegrationService;
@@ -47,9 +51,9 @@ use user_service::UserService;
 /// All the services that are used by the app
 pub struct AppServices {
     pub app_router: Router,
+    pub fitness_service: Arc<FitnessService>,
     pub importer_service: Arc<ImporterService>,
     pub exporter_service: Arc<ExporterService>,
-    pub fitness_service: Arc<FitnessService>,
     pub statistics_service: Arc<StatisticsService>,
     pub integration_service: Arc<IntegrationService>,
     pub miscellaneous_service: Arc<MiscellaneousService>,
@@ -61,8 +65,9 @@ pub async fn create_app_services(
     timezone: chrono_tz::Tz,
     s3_client: aws_sdk_s3::Client,
     config: Arc<config::AppConfig>,
-    perform_application_job: &MemoryStorage<ApplicationJob>,
-    perform_core_application_job: &MemoryStorage<CoreApplicationJob>,
+    lp_application_job: &MemoryStorage<LpApplicationJob>,
+    mp_application_job: &MemoryStorage<MpApplicationJob>,
+    hp_application_job: &MemoryStorage<HpApplicationJob>,
 ) -> AppServices {
     let oidc_client = create_oidc_client(&config).await;
     let file_storage_service = Arc::new(FileStorageService::new(
@@ -78,8 +83,9 @@ pub async fn create_app_services(
             config.clone(),
             oidc_client,
             file_storage_service.clone(),
-            perform_application_job,
-            perform_core_application_job,
+            lp_application_job,
+            mp_application_job,
+            hp_application_job,
         )
         .await,
     );
@@ -124,6 +130,15 @@ pub async fn create_app_services(
     let webhook_routes =
         Router::new().route("/integrations/:integration_slug", post(integration_webhook));
 
+    join_all(
+        [
+            MpApplicationJob::SyncIntegrationsData,
+            MpApplicationJob::UpdateExerciseLibrary,
+        ]
+        .map(|job| supporting_service.perform_application_job(ApplicationJob::Mp(job))),
+    )
+    .await;
+
     let mut gql = post(graphql_handler);
     if config.server.graphql_playground_enabled {
         gql = gql.get(graphql_playground);
@@ -143,11 +158,12 @@ pub async fn create_app_services(
             1024 * 1024 * config.server.max_file_size,
         ))
         .layer(cors);
+
     AppServices {
         app_router,
+        fitness_service,
         importer_service,
         exporter_service,
-        fitness_service,
         statistics_service,
         integration_service,
         miscellaneous_service,
diff --git a/apps/backend/src/job.rs b/apps/backend/src/job.rs
index 34a7b23b2d..660d83116b 100644
--- a/apps/backend/src/job.rs
+++ b/apps/backend/src/job.rs
@@ -1,13 +1,12 @@
-use std::{sync::Arc, time::Instant};
+use std::sync::Arc;
 
 use apalis::prelude::*;
-use background::{ApplicationJob, CoreApplicationJob, ScheduledJob};
+use background_models::{HpApplicationJob, LpApplicationJob, MpApplicationJob, ScheduledJob};
 use common_utils::ryot_log;
 use exporter_service::ExporterService;
 use fitness_service::FitnessService;
 use importer_service::ImporterService;
 use integration_service::IntegrationService;
-use media_models::CommitMediaInput;
 use miscellaneous_service::MiscellaneousService;
 use statistics_service::StatisticsService;
 use traits::TraceOk;
@@ -37,108 +36,76 @@ pub async fn run_frequent_jobs(
     Ok(())
 }
 
-// Application Jobs
-
-pub async fn perform_core_application_job(
-    information: CoreApplicationJob,
-    integration_service: Data<Arc<IntegrationService>>,
+pub async fn perform_hp_application_job(
+    information: HpApplicationJob,
     misc_service: Data<Arc<MiscellaneousService>>,
+    statistics_service: Data<Arc<StatisticsService>>,
+    integration_service: Data<Arc<IntegrationService>>,
 ) -> Result<(), Error> {
-    let name = information.to_string();
     ryot_log!(trace, "Started job {:?}", information);
-    let start = Instant::now();
     let status = match information {
-        CoreApplicationJob::SyncIntegrationsData(user_id) => integration_service
-            .yank_integrations_data_for_user(&user_id)
-            .await
-            .is_ok(),
-        CoreApplicationJob::ReviewPosted(event) => {
-            misc_service.handle_review_posted_event(event).await.is_ok()
+        HpApplicationJob::SyncUserIntegrationsData(user_id) => {
+            integration_service
+                .yank_integrations_data_for_user(&user_id)
+                .await
+        }
+        HpApplicationJob::RecalculateUserActivitiesAndSummary(
+            user_id,
+            calculate_from_beginning,
+        ) => {
+            statistics_service
+                .calculate_user_activities_and_summary(&user_id, calculate_from_beginning)
+                .await
+        }
+        HpApplicationJob::ReviewPosted(event) => {
+            misc_service.handle_review_posted_event(event).await
+        }
+        HpApplicationJob::BulkProgressUpdate(user_id, input) => {
+            misc_service.bulk_progress_update(user_id, input).await
         }
-        CoreApplicationJob::BulkProgressUpdate(user_id, input) => misc_service
-            .bulk_progress_update(user_id, input)
-            .await
-            .is_ok(),
     };
-    ryot_log!(
-        trace,
-        "Job: {:#?}, Time Taken: {}ms, Successful = {}",
-        name,
-        (Instant::now() - start).as_millis(),
-        status
-    );
-    Ok(())
+    status.map_err(|e| Error::Failed(Arc::new(e.message.into())))
 }
 
-pub async fn perform_application_job(
-    information: ApplicationJob,
+pub async fn perform_mp_application_job(
+    information: MpApplicationJob,
     misc_service: Data<Arc<MiscellaneousService>>,
     integration_service: Data<Arc<IntegrationService>>,
     importer_service: Data<Arc<ImporterService>>,
     exporter_service: Data<Arc<ExporterService>>,
     fitness_service: Data<Arc<FitnessService>>,
-    statistics_service: Data<Arc<StatisticsService>>,
 ) -> Result<(), Error> {
-    let name = information.to_string();
     ryot_log!(trace, "Started job {:?}", information);
-    let start = Instant::now();
     let status = match information {
-        ApplicationJob::ImportFromExternalSource(user_id, input) => importer_service
-            .start_importing(user_id, input)
-            .await
-            .is_ok(),
-        ApplicationJob::RecalculateUserActivitiesAndSummary(user_id, calculate_from_beginning) => {
-            statistics_service
-                .calculate_user_activities_and_summary(&user_id, calculate_from_beginning)
+        MpApplicationJob::ImportFromExternalSource(user_id, input) => {
+            importer_service.perform_import(user_id, input).await
+        }
+        MpApplicationJob::ReviseUserWorkouts(user_id) => {
+            fitness_service.revise_user_workouts(user_id).await
+        }
+        MpApplicationJob::UpdateMetadata(metadata_id) => {
+            misc_service
+                .update_metadata_and_notify_users(&metadata_id)
                 .await
-                .is_ok()
         }
-        ApplicationJob::ReviseUserWorkouts(user_id) => {
-            fitness_service.revise_user_workouts(user_id).await.is_ok()
+        MpApplicationJob::UpdatePerson(person_id) => {
+            misc_service
+                .update_person_and_notify_users(&person_id)
+                .await
         }
-        ApplicationJob::UpdateMetadata(metadata_id, force_update) => misc_service
-            .update_metadata_and_notify_users(&metadata_id, force_update)
-            .await
-            .is_ok(),
-        ApplicationJob::UpdatePerson(person_id) => misc_service
-            .update_person_and_notify_users(&person_id)
-            .await
-            .is_ok(),
-        ApplicationJob::HandleAfterMediaSeenTasks(seen) => misc_service
-            .handle_after_media_seen_tasks(seen)
-            .await
-            .is_ok(),
-        ApplicationJob::UpdateMetadataGroup(metadata_group_id) => misc_service
-            .update_metadata_group(&metadata_group_id)
-            .await
-            .is_ok(),
-        ApplicationJob::UpdateGithubExerciseJob(exercise) => fitness_service
-            .update_github_exercise(exercise)
-            .await
-            .is_ok(),
-        ApplicationJob::RecalculateCalendarEvents => {
-            misc_service.recalculate_calendar_events().await.is_ok()
+        MpApplicationJob::UpdateMetadataGroup(metadata_group_id) => {
+            misc_service.update_metadata_group(&metadata_group_id).await
         }
-        ApplicationJob::PerformBackgroundTasks => {
-            misc_service.perform_background_jobs().await.is_ok()
+        MpApplicationJob::UpdateGithubExercises => fitness_service.update_github_exercises().await,
+        MpApplicationJob::RecalculateCalendarEvents => {
+            misc_service.recalculate_calendar_events().await
         }
-        ApplicationJob::AssociateGroupWithMetadata(lot, source, identifier) => misc_service
-            .commit_metadata_group(CommitMediaInput {
-                lot,
-                source,
-                identifier,
-                force_update: None,
-            })
-            .await
-            .is_ok(),
-        ApplicationJob::PerformExport(user_id) => {
-            exporter_service.perform_export(user_id).await.is_ok()
+        MpApplicationJob::PerformBackgroundTasks => misc_service.perform_background_jobs().await,
+        MpApplicationJob::PerformExport(user_id) => exporter_service.perform_export(user_id).await,
+        MpApplicationJob::UpdateExerciseLibrary => {
+            fitness_service.deploy_update_exercise_library_job().await
         }
-        ApplicationJob::UpdateExerciseLibrary => fitness_service
-            .deploy_update_exercise_library_job()
-            .await
-            .is_ok(),
-        ApplicationJob::SyncIntegrationsData => {
+        MpApplicationJob::SyncIntegrationsData => {
             integration_service
                 .yank_integrations_data()
                 .await
@@ -146,25 +113,29 @@ pub async fn perform_application_job(
             integration_service
                 .sync_integrations_data_to_owned_collection()
                 .await
-                .is_ok()
         }
-        ApplicationJob::HandleEntityAddedToCollectionEvent(collection_to_entity_id) => {
+    };
+    status.map_err(|e| Error::Failed(Arc::new(e.message.into())))
+}
+
+pub async fn perform_lp_application_job(
+    information: LpApplicationJob,
+    misc_service: Data<Arc<MiscellaneousService>>,
+    integration_service: Data<Arc<IntegrationService>>,
+) -> Result<(), Error> {
+    ryot_log!(trace, "Started job {:?}", information);
+    let status = match information {
+        LpApplicationJob::HandleAfterMediaSeenTasks(seen) => {
+            misc_service.handle_after_media_seen_tasks(seen).await
+        }
+        LpApplicationJob::HandleEntityAddedToCollectionEvent(collection_to_entity_id) => {
             integration_service
                 .handle_entity_added_to_collection_event(collection_to_entity_id)
                 .await
-                .is_ok()
         }
-        ApplicationJob::HandleOnSeenComplete(id) => integration_service
-            .handle_on_seen_complete(id)
-            .await
-            .is_ok(),
+        LpApplicationJob::HandleOnSeenComplete(id) => {
+            integration_service.handle_on_seen_complete(id).await
+        }
     };
-    ryot_log!(
-        trace,
-        "Job: {:#?}, Time Taken: {}ms, Successful = {}",
-        name,
-        (Instant::now() - start).as_millis(),
-        status
-    );
-    Ok(())
+    status.map_err(|e| Error::Failed(Arc::new(e.message.into())))
 }
diff --git a/apps/backend/src/main.rs b/apps/backend/src/main.rs
index dc86f45729..b7a508da25 100644
--- a/apps/backend/src/main.rs
+++ b/apps/backend/src/main.rs
@@ -9,17 +9,18 @@ use std::{
 
 use anyhow::{bail, Result};
 use apalis::{
-    layers::{limit::RateLimitLayer as ApalisRateLimitLayer, WorkerBuilderExt},
-    prelude::{MemoryStorage, MessageQueue, Monitor, WorkerBuilder, WorkerFactoryFn},
+    layers::{retry::RetryPolicy, WorkerBuilderExt},
+    prelude::{MemoryStorage, Monitor, WorkerBuilder, WorkerFactoryFn},
 };
 use apalis_cron::{CronStream, Schedule};
 use aws_sdk_s3::config::Region;
-use background::ApplicationJob;
 use common_utils::{ryot_log, PROJECT_NAME, TEMP_DIR};
+use dependent_models::CompleteExport;
 use env_utils::APP_VERSION;
-use futures::future::join_all;
+use job::perform_lp_application_job;
 use logs_wheel::LogFileInitializer;
 use migrations::Migrator;
+use schematic::schema::{SchemaGenerator, TypeScriptRenderer, YamlTemplateRenderer};
 use sea_orm::{ConnectionTrait, Database, DatabaseConnection};
 use sea_orm_migration::MigratorTrait;
 use tokio::{
@@ -32,7 +33,7 @@ use tracing_subscriber::{fmt, layer::SubscriberExt};
 use crate::{
     common::create_app_services,
     job::{
-        perform_application_job, perform_core_application_job, run_background_jobs,
+        perform_hp_application_job, perform_mp_application_job, run_background_jobs,
         run_frequent_jobs,
     },
 };
@@ -66,7 +67,6 @@ async fn main() -> Result<()> {
         sleep(duration).await;
     }
 
-    let rate_limit_count = config.scheduler.rate_limit_num;
     let sync_every_minutes = config.integration.sync_every_minutes;
     let disable_background_jobs = config.server.disable_background_jobs;
 
@@ -107,38 +107,27 @@ async fn main() -> Result<()> {
         bail!("There was an error running the database migrations.");
     };
 
-    let perform_application_job_storage = MemoryStorage::new();
-    let perform_core_application_job_storage = MemoryStorage::new();
+    let lp_application_job_storage = MemoryStorage::new();
+    let mp_application_job_storage = MemoryStorage::new();
+    let hp_application_job_storage = MemoryStorage::new();
 
     let tz: chrono_tz::Tz = env::var("TZ")
         .map(|s| s.parse().unwrap())
         .unwrap_or_else(|_| chrono_tz::Etc::GMT);
     ryot_log!(info, "Timezone: {}", tz);
 
-    join_all([
-        perform_application_job_storage
-            .clone()
-            .enqueue(ApplicationJob::SyncIntegrationsData),
-        perform_application_job_storage
-            .clone()
-            .enqueue(ApplicationJob::UpdateExerciseLibrary),
-    ])
-    .await;
-
     let app_services = create_app_services(
         db,
         tz,
         s3_client,
         config,
-        &perform_application_job_storage,
-        &perform_core_application_job_storage,
+        &lp_application_job_storage,
+        &mp_application_job_storage,
+        &hp_application_job_storage,
     )
     .await;
 
     if cfg!(debug_assertions) {
-        use dependent_models::CompleteExport;
-        use schematic::schema::{SchemaGenerator, TypeScriptRenderer, YamlTemplateRenderer};
-
         // TODO: Once https://github.com/rust-lang/cargo/issues/3946 is resolved
         let base_dir = PathBuf::from(BASE_DIR)
             .parent()
@@ -175,23 +164,12 @@ async fn main() -> Result<()> {
     let listener = TcpListener::bind(format!("{host}:{port}")).await.unwrap();
     ryot_log!(info, "Listening on: {}", listener.local_addr()?);
 
-    let fitness_service_1 = app_services.fitness_service.clone();
-    let fitness_service_2 = app_services.fitness_service.clone();
-    let importer_service_1 = app_services.importer_service.clone();
-    let exporter_service_1 = app_services.exporter_service.clone();
-    let statistics_service_1 = app_services.statistics_service.clone();
-    let integration_service_1 = app_services.integration_service.clone();
-    let integration_service_2 = app_services.integration_service.clone();
-    let integration_service_3 = app_services.integration_service.clone();
-    let miscellaneous_service_1 = app_services.miscellaneous_service.clone();
-    let miscellaneous_service_2 = app_services.miscellaneous_service.clone();
-    let miscellaneous_service_3 = app_services.miscellaneous_service.clone();
-
     let monitor = Monitor::new()
         .register(
             WorkerBuilder::new("daily_background_jobs")
                 .enable_tracing()
-                .data(miscellaneous_service_1.clone())
+                .catch_panic()
+                .data(app_services.miscellaneous_service.clone())
                 .backend(
                     // every day
                     CronStream::new_with_timezone(Schedule::from_str("0 0 0 * * *").unwrap(), tz),
@@ -201,8 +179,9 @@ async fn main() -> Result<()> {
         .register(
             WorkerBuilder::new("frequent_jobs")
                 .enable_tracing()
-                .data(integration_service_1.clone())
-                .data(fitness_service_2.clone())
+                .catch_panic()
+                .data(app_services.integration_service.clone())
+                .data(app_services.fitness_service.clone())
                 .backend(CronStream::new_with_timezone(
                     Schedule::from_str(&format!("0 */{} * * * *", sync_every_minutes)).unwrap(),
                     tz,
@@ -211,28 +190,38 @@ async fn main() -> Result<()> {
         )
         // application jobs
         .register(
-            WorkerBuilder::new("perform_core_application_job")
+            WorkerBuilder::new("perform_hp_application_job")
+                .catch_panic()
                 .enable_tracing()
-                .data(integration_service_2.clone())
-                .data(miscellaneous_service_3.clone())
-                .backend(perform_core_application_job_storage)
-                .build_fn(perform_core_application_job),
+                .data(app_services.statistics_service.clone())
+                .data(app_services.integration_service.clone())
+                .data(app_services.miscellaneous_service.clone())
+                .backend(hp_application_job_storage)
+                .build_fn(perform_hp_application_job),
         )
         .register(
-            WorkerBuilder::new("perform_application_job")
+            WorkerBuilder::new("perform_mp_application_job")
+                .catch_panic()
                 .enable_tracing()
-                .data(fitness_service_1.clone())
-                .data(exporter_service_1.clone())
-                .data(importer_service_1.clone())
-                .data(statistics_service_1.clone())
-                .data(integration_service_3.clone())
-                .data(miscellaneous_service_2.clone())
-                .layer(ApalisRateLimitLayer::new(
-                    rate_limit_count,
-                    Duration::new(5, 0),
-                ))
-                .backend(perform_application_job_storage)
-                .build_fn(perform_application_job),
+                .rate_limit(5, Duration::new(5, 0))
+                .retry(RetryPolicy::retries(3))
+                .data(app_services.fitness_service.clone())
+                .data(app_services.exporter_service.clone())
+                .data(app_services.importer_service.clone())
+                .data(app_services.integration_service.clone())
+                .data(app_services.miscellaneous_service.clone())
+                .backend(mp_application_job_storage)
+                .build_fn(perform_mp_application_job),
+        )
+        .register(
+            WorkerBuilder::new("perform_lp_application_job")
+                .catch_panic()
+                .enable_tracing()
+                .rate_limit(20, Duration::new(5, 0))
+                .data(app_services.integration_service)
+                .data(app_services.miscellaneous_service)
+                .backend(lp_application_job_storage)
+                .build_fn(perform_lp_application_job),
         )
         .run();
 
@@ -251,9 +240,9 @@ fn init_tracing() -> Result<()> {
     let tmp_dir = PathBuf::new().join(TEMP_DIR);
     create_dir_all(&tmp_dir)?;
     let log_file = LogFileInitializer {
+        max_n_old_files: 2,
         directory: tmp_dir,
         filename: PROJECT_NAME,
-        max_n_old_files: 2,
         preferred_max_file_size_mib: 1,
     }
     .init()?;
diff --git a/apps/frontend/app/components/common.tsx b/apps/frontend/app/components/common.tsx
index 1f2fb8cd69..394a6c1ad2 100644
--- a/apps/frontend/app/components/common.tsx
+++ b/apps/frontend/app/components/common.tsx
@@ -1,5 +1,6 @@
 import { Carousel } from "@mantine/carousel";
 import "@mantine/carousel/styles.css";
+import { useAutoAnimate } from "@formkit/auto-animate/react";
 import {
 	ActionIcon,
 	Alert,
@@ -104,14 +105,16 @@ export const ApplicationGrid = (props: {
 	children: ReactNode | Array<ReactNode>;
 }) => {
 	const userPreferences = useUserPreferences();
+	const [parent] = useAutoAnimate();
 
 	return (
 		<SimpleGrid
+			spacing="lg"
+			ref={parent}
 			cols={match(userPreferences.general.gridPacking)
 				.with(GridPacking.Normal, () => ({ base: 2, sm: 3, md: 4, lg: 5 }))
 				.with(GridPacking.Dense, () => ({ base: 3, sm: 4, md: 5, lg: 6 }))
 				.exhaustive()}
-			spacing="lg"
 		>
 			{props.children}
 		</SimpleGrid>
@@ -267,6 +270,7 @@ export const BaseMediaDisplayItem = (props: {
 		bottomLeft?: ReactNode;
 	};
 }) => {
+	const coreDetails = useCoreDetails();
 	const userPreferences = useUserPreferences();
 	const gridPacking = userPreferences.general.gridPacking;
 	const SurroundingElement = (iProps: { children: ReactNode }) =>
@@ -292,7 +296,8 @@ export const BaseMediaDisplayItem = (props: {
 							pos="relative"
 							style={{ overflow: "hidden" }}
 							className={clsx({
-								[classes.highlightImage]: props.highlightImage,
+								[classes.highlightImage]:
+									coreDetails.isServerKeyValidated && props.highlightImage,
 							})}
 						>
 							<Image
diff --git a/apps/frontend/app/lib/generals.ts b/apps/frontend/app/lib/generals.ts
index 91904f1dda..9fced0afcc 100644
--- a/apps/frontend/app/lib/generals.ts
+++ b/apps/frontend/app/lib/generals.ts
@@ -349,12 +349,6 @@ export const getSurroundingElements = <T>(
 	return [elementIndex - 1, elementIndex, elementIndex + 1];
 };
 
-const usersQueryKeys = createQueryKeys("users", {
-	details: (token: string) => ({
-		queryKey: ["userDetails", token],
-	}),
-});
-
 const mediaQueryKeys = createQueryKeys("media", {
 	metadataPartialDetails: (metadataId: string) => ({
 		queryKey: ["metadataPartialDetails", metadataId],
@@ -383,12 +377,6 @@ const mediaQueryKeys = createQueryKeys("media", {
 });
 
 const collectionQueryKeys = createQueryKeys("collections", {
-	userList: (userId: string) => ({
-		queryKey: ["userCollectionsList", userId],
-	}),
-	details: (collectionId: string, take?: number) => ({
-		queryKey: ["collectionDetails", collectionId, take],
-	}),
 	images: (collectionId: string) => ({
 		queryKey: ["collectionDetails", "images", collectionId],
 	}),
@@ -409,12 +397,6 @@ const fitnessQueryKeys = createQueryKeys("fitness", {
 	}),
 });
 
-const miscellaneousQueryKeys = createQueryKeys("miscellaneous", {
-	coreDetails: () => ({
-		queryKey: ["coreDetails"],
-	}),
-});
-
 const analyticsQueryKeys = createQueryKeys("analytics", {
 	user: (input: UserAnalyticsQueryVariables) => ({
 		queryKey: ["user", input],
@@ -422,12 +404,10 @@ const analyticsQueryKeys = createQueryKeys("analytics", {
 });
 
 export const queryFactory = mergeQueryKeys(
-	usersQueryKeys,
 	mediaQueryKeys,
 	fitnessQueryKeys,
 	analyticsQueryKeys,
 	collectionQueryKeys,
-	miscellaneousQueryKeys,
 );
 
 export const getPartialMetadataDetailsQuery = (metadataId: string) =>
diff --git a/apps/frontend/app/lib/utilities.server.ts b/apps/frontend/app/lib/utilities.server.ts
index 0532daad47..6b3c33829a 100644
--- a/apps/frontend/app/lib/utilities.server.ts
+++ b/apps/frontend/app/lib/utilities.server.ts
@@ -31,12 +31,9 @@ import { v4 as randomUUID } from "uuid";
 import { z } from "zod";
 import {
 	FRONTEND_AUTH_COOKIE_NAME,
-	dayjsLib,
 	emptyDecimalString,
 	emptyNumberString,
 	pageQueryParam,
-	queryClient,
-	queryFactory,
 	redirectToQueryParam,
 	toastKey,
 } from "~/lib/generals";
@@ -105,7 +102,7 @@ export const getAuthorizationCookie = (request: Request) =>
 
 export const redirectIfNotAuthenticatedOrUpdated = async (request: Request) => {
 	try {
-		const userDetails = await getCachedUserDetails(request);
+		const userDetails = await getUserDetails(request);
 		const getResponseInit = async (toastMessage: string) => ({
 			status: 302,
 			headers: combineHeaders(
@@ -180,39 +177,27 @@ export const getCoreDetails = async () => {
 		.then((d) => d.coreDetails);
 };
 
-const getCachedUserDetails = async (request: Request) => {
-	const decodedJwt = getDecodedJwt(request);
-	return await queryClient.ensureQueryData({
-		queryKey: queryFactory.users.details(decodedJwt.sub).queryKey,
-		queryFn: () =>
-			serverGqlService
-				.authenticatedRequest(request, UserDetailsDocument, undefined)
-				.then((d) => d.userDetails),
-	});
+const getUserDetails = async (request: Request) => {
+	const { userDetails } = await serverGqlService.authenticatedRequest(
+		request,
+		UserDetailsDocument,
+		undefined,
+	);
+	return userDetails;
 };
 
-export const getCachedUserPreferences = async (request: Request) => {
+export const getUserPreferences = async (request: Request) => {
 	const userDetails = await redirectIfNotAuthenticatedOrUpdated(request);
 	return userDetails.preferences;
 };
 
-export const getCachedUserCollectionsList = async (request: Request) => {
-	const userDetails = await redirectIfNotAuthenticatedOrUpdated(request);
-	return queryClient.ensureQueryData({
-		queryKey: queryFactory.collections.userList(userDetails.id).queryKey,
-		queryFn: () =>
-			serverGqlService
-				.authenticatedRequest(request, UserCollectionsListDocument, {})
-				.then((data) => data.userCollectionsList),
-		staleTime: dayjsLib.duration(1, "hour").asMilliseconds(),
-	});
-};
-
-export const removeCachedUserCollectionsList = async (request: Request) => {
-	const userDetails = await redirectIfNotAuthenticatedOrUpdated(request);
-	queryClient.removeQueries({
-		queryKey: queryFactory.collections.userList(userDetails.id).queryKey,
-	});
+export const getUserCollectionsList = async (request: Request) => {
+	const { userCollectionsList } = await serverGqlService.authenticatedRequest(
+		request,
+		UserCollectionsListDocument,
+		{},
+	);
+	return userCollectionsList;
 };
 
 export const uploadFileAndGetKey = async (
@@ -384,7 +369,7 @@ export const redirectUsingEnhancedCookieSearchParams = async (
 	request: Request,
 	cookieName: string,
 ) => {
-	const preferences = await getCachedUserPreferences(request);
+	const preferences = await getUserPreferences(request);
 	const { searchParams } = new URL(request.url);
 	if (searchParams.size > 0 || !preferences.general.persistQueries) return;
 	const cookies = parse(request.headers.get("cookie") || "");
diff --git a/apps/frontend/app/root.tsx b/apps/frontend/app/root.tsx
index 88b8965aed..fa5f603cca 100644
--- a/apps/frontend/app/root.tsx
+++ b/apps/frontend/app/root.tsx
@@ -102,24 +102,10 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
 	const defaultColorScheme = colorScheme || "light";
 	if (toastHeaders) extendResponseHeaders(headers, toastHeaders);
 
-	const userAgent = request.headers.get("user-agent") || "";
-	const isIOS = /iPad|iPhone|iPod/.test(userAgent);
-	let isIOS18 = false;
-
-	if (isIOS) {
-		const match = userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/);
-		if (match) {
-			const version = Number.parseInt(match[1], 10);
-			isIOS18 = version >= 18;
-		}
-	}
-
-	return data({ toast, defaultColorScheme, isIOS18 }, { headers });
+	return data({ toast, defaultColorScheme }, { headers });
 };
 
 const DefaultHeadTags = () => {
-	const loaderData = useLoaderData<typeof loader>();
-
 	return (
 		<>
 			<meta charSet="utf-8" />
@@ -128,12 +114,7 @@ const DefaultHeadTags = () => {
 				content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, user-scalable=no, viewport-fit=cover"
 			/>
 			<link rel="manifest" href="/manifest.json" />
-			<link
-				rel="apple-touch-icon"
-				href={
-					loaderData.isIOS18 ? "/icon-192x192.png" : "/apple-touch-icon.png"
-				}
-			/>
+			<link rel="apple-touch-icon" href={"/icon-192x192.png"} />
 		</>
 	);
 };
@@ -153,18 +134,17 @@ export default function App() {
 			<body>
 				<QueryClientProvider client={queryClient}>
 					<MantineProvider
-						classNamesPrefix="mnt"
 						theme={theme}
+						classNamesPrefix="mnt"
 						forceColorScheme={loaderData.defaultColorScheme}
 					>
 						<ConfirmationMountPoint />
-						{navigation.state === "loading" ||
-						navigation.state === "submitting" ? (
+						{["loading", "submitting"].includes(navigation.state) ? (
 							<Loader
-								pos="fixed"
-								right={10}
 								top={10}
 								size="sm"
+								right={10}
+								pos="fixed"
 								color="yellow"
 								style={{ zIndex: 10 }}
 							/>
diff --git a/apps/frontend/app/routes/_dashboard._index.tsx b/apps/frontend/app/routes/_dashboard._index.tsx
index 329426b0c2..1678b0bebe 100644
--- a/apps/frontend/app/routes/_dashboard._index.tsx
+++ b/apps/frontend/app/routes/_dashboard._index.tsx
@@ -56,8 +56,8 @@ import {
 	useUserUnitSystem,
 } from "~/lib/hooks";
 import {
-	getCachedUserCollectionsList,
-	getCachedUserPreferences,
+	getUserCollectionsList,
+	getUserPreferences,
 	serverGqlService,
 } from "~/lib/utilities.server";
 
@@ -70,7 +70,7 @@ const getTake = (preferences: UserPreferences, el: DashboardElementLot) => {
 };
 
 export const loader = async ({ request }: LoaderFunctionArgs) => {
-	const preferences = await getCachedUserPreferences(request);
+	const preferences = await getUserPreferences(request);
 	const takeUpcoming = getTake(preferences, DashboardElementLot.Upcoming);
 	const takeInProgress = getTake(preferences, DashboardElementLot.InProgress);
 	const getRecommendations = async () => {
@@ -87,7 +87,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
 		);
 		return userRecommendations;
 	};
-	const userCollectionsList = await getCachedUserCollectionsList(request);
+	const userCollectionsList = await getUserCollectionsList(request);
 	const foundInProgressCollection = userCollectionsList.find(
 		(c) => c.name === "In Progress",
 	);
diff --git a/apps/frontend/app/routes/_dashboard.analytics.tsx b/apps/frontend/app/routes/_dashboard.analytics.tsx
index 2f99c66c1d..8206838d5e 100644
--- a/apps/frontend/app/routes/_dashboard.analytics.tsx
+++ b/apps/frontend/app/routes/_dashboard.analytics.tsx
@@ -109,7 +109,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
 };
 
 export const meta = (_args: MetaArgs<typeof loader>) => {
-	return [{ title: "Fitness Analytics | Ryot" }];
+	return [{ title: "Analytics | Ryot" }];
 };
 
 const useTimeSpanSettings = () => {
@@ -256,10 +256,10 @@ export default function Page() {
 								<ExercisesChart />
 							</Grid.Col>
 							<Grid.Col span={{ base: 12, md: 6 }}>
-								<TimeOfDayChart />
+								<StatisticsCard />
 							</Grid.Col>
 							<Grid.Col span={{ base: 12, md: 6 }}>
-								<StatisticsCard />
+								<TimeOfDayChart />
 							</Grid.Col>
 							<Grid.Col span={12}>
 								<ActivitySection />
diff --git a/apps/frontend/app/routes/_dashboard.collections.list.tsx b/apps/frontend/app/routes/_dashboard.collections.list.tsx
index d800b7770a..79c0b3f8af 100644
--- a/apps/frontend/app/routes/_dashboard.collections.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.collections.list.tsx
@@ -91,7 +91,6 @@ import {
 	createToastHeaders,
 	getEnhancedCookieName,
 	redirectUsingEnhancedCookieSearchParams,
-	removeCachedUserCollectionsList,
 	serverGqlService,
 } from "~/lib/utilities.server";
 
@@ -110,7 +109,6 @@ export const meta = (_args: MetaArgs<typeof loader>) => {
 
 export const action = async ({ request }: ActionFunctionArgs) => {
 	const formData = await request.clone().formData();
-	await removeCachedUserCollectionsList(request);
 	const intent = getActionIntent(request);
 	return await match(intent)
 		.with("createOrUpdate", async () => {
diff --git a/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx b/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
index bb825446bd..15b46745bd 100644
--- a/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
@@ -457,6 +457,7 @@ const MediaSearchItem = (props: {
 		if (props.maybeItemId) return props.maybeItemId;
 		setIsLoading(true);
 		const data = new FormData();
+		data.append("name", props.item.item.title);
 		data.append("identifier", props.item.item.identifier);
 		data.append("lot", props.lot);
 		data.append("source", props.source);
diff --git a/apps/frontend/app/routes/_dashboard.media.groups.$action.tsx b/apps/frontend/app/routes/_dashboard.media.groups.$action.tsx
index 87fd6e7583..7f0923f4ab 100644
--- a/apps/frontend/app/routes/_dashboard.media.groups.$action.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.groups.$action.tsx
@@ -354,6 +354,7 @@ const GroupSearchItem = (props: {
 				if (loaderData.search) {
 					setIsLoading(true);
 					const id = await commitGroup(
+						props.item.name,
 						props.item.identifier,
 						loaderData.search.url.source,
 						loaderData.search.lot,
@@ -367,11 +368,13 @@ const GroupSearchItem = (props: {
 };
 
 const commitGroup = async (
+	name: string,
 	identifier: string,
 	source: MediaSource,
 	lot: MediaLot,
 ) => {
 	const data = new FormData();
+	data.append("name", name);
 	data.append("identifier", identifier);
 	data.append("source", source);
 	data.append("lot", lot);
diff --git a/apps/frontend/app/routes/_dashboard.settings.preferences.tsx b/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
index fc2551581e..3b31d301d9 100644
--- a/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
@@ -59,7 +59,6 @@ import { match } from "ts-pattern";
 import { z } from "zod";
 import { zx } from "zodix";
 import { confirmWrapper } from "~/components/confirmation";
-import { queryClient, queryFactory } from "~/lib/generals";
 import {
 	useComplexJsonUpdate,
 	useConfirmSubmit,
@@ -67,11 +66,7 @@ import {
 	useIsFitnessActionActive,
 	useUserPreferences,
 } from "~/lib/hooks";
-import {
-	createToastHeaders,
-	redirectIfNotAuthenticatedOrUpdated,
-	serverGqlService,
-} from "~/lib/utilities.server";
+import { createToastHeaders, serverGqlService } from "~/lib/utilities.server";
 import classes from "~/styles/preferences.module.css";
 
 const searchSchema = z.object({
@@ -95,7 +90,6 @@ const notificationContent = {
 };
 
 export const action = async ({ request }: ActionFunctionArgs) => {
-	const userDetails = await redirectIfNotAuthenticatedOrUpdated(request);
 	const entries = Object.entries(Object.fromEntries(await request.formData()));
 	const submission = [];
 	for (let [property, value] of entries) {
@@ -115,9 +109,6 @@ export const action = async ({ request }: ActionFunctionArgs) => {
 			{ input },
 		);
 	}
-	queryClient.removeQueries({
-		queryKey: queryFactory.users.details(userDetails.id).queryKey,
-	});
 	const toastHeaders = await createToastHeaders({
 		message: "Preferences updated",
 		type: "success",
diff --git a/apps/frontend/app/routes/_dashboard.settings.profile-and-sharing.tsx b/apps/frontend/app/routes/_dashboard.settings.profile-and-sharing.tsx
index 881b807a1c..0d10dd7eb3 100644
--- a/apps/frontend/app/routes/_dashboard.settings.profile-and-sharing.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.profile-and-sharing.tsx
@@ -55,12 +55,7 @@ import { withQuery } from "ufo";
 import { z } from "zod";
 import { zx } from "zodix";
 import { confirmWrapper } from "~/components/confirmation";
-import {
-	applicationBaseUrl,
-	dayjsLib,
-	queryClient,
-	queryFactory,
-} from "~/lib/generals";
+import { applicationBaseUrl, dayjsLib } from "~/lib/generals";
 import {
 	useConfirmSubmit,
 	useCoreDetails,
@@ -69,7 +64,6 @@ import {
 } from "~/lib/hooks";
 import {
 	createToastHeaders,
-	getAuthorizationCookie,
 	getDecodedJwt,
 	serverGqlService,
 } from "~/lib/utilities.server";
@@ -91,14 +85,10 @@ export const action = async ({ request }: ActionFunctionArgs) => {
 	const intent = getActionIntent(request);
 	return await match(intent)
 		.with("updateProfile", async () => {
-			const token = getAuthorizationCookie(request);
 			const submission = processSubmission(formData, updateProfileFormSchema);
 			await serverGqlService.authenticatedRequest(request, UpdateUserDocument, {
 				input: submission,
 			});
-			queryClient.removeQueries({
-				queryKey: queryFactory.users.details(token ?? "").queryKey,
-			});
 			return Response.json({ status: "success", submission } as const, {
 				headers: await createToastHeaders({
 					type: "success",
diff --git a/apps/frontend/app/routes/_dashboard.tsx b/apps/frontend/app/routes/_dashboard.tsx
index aba2196ebe..8458335f8c 100644
--- a/apps/frontend/app/routes/_dashboard.tsx
+++ b/apps/frontend/app/routes/_dashboard.tsx
@@ -140,11 +140,11 @@ import {
 	useReviewEntity,
 } from "~/lib/state/media";
 import {
-	getCachedUserCollectionsList,
-	getCachedUserPreferences,
+	getUserCollectionsList,
 	getCookieValue,
 	getCoreDetails,
 	getDecodedJwt,
+	getUserPreferences,
 	redirectIfNotAuthenticatedOrUpdated,
 } from "~/lib/utilities.server";
 import { colorSchemeCookie } from "~/lib/utilities.server";
@@ -162,8 +162,8 @@ const desktopSidebarCollapsedCookie = "DesktopSidebarCollapsed";
 export const loader = async ({ request }: LoaderFunctionArgs) => {
 	const userDetails = await redirectIfNotAuthenticatedOrUpdated(request);
 	const [userPreferences, userCollections, coreDetails] = await Promise.all([
-		getCachedUserPreferences(request),
-		getCachedUserCollectionsList(request),
+		getUserPreferences(request),
+		getUserCollectionsList(request),
 		getCoreDetails(),
 	]);
 	const desktopSidebarCollapsed = getCookieValue(
diff --git a/apps/frontend/app/routes/actions.tsx b/apps/frontend/app/routes/actions.tsx
index 2901c11191..bbe3b743ac 100644
--- a/apps/frontend/app/routes/actions.tsx
+++ b/apps/frontend/app/routes/actions.tsx
@@ -45,7 +45,6 @@ import {
 	createToastHeaders,
 	extendResponseHeaders,
 	getLogoutCookies,
-	removeCachedUserCollectionsList,
 	s3FileUploader,
 	serverGqlService,
 } from "~/lib/utilities.server";
@@ -69,7 +68,16 @@ export const action = async ({ request }: ActionFunctionArgs) => {
 			const { commitMetadata } = await serverGqlService.authenticatedRequest(
 				request,
 				CommitMetadataDocument,
-				{ input: submission },
+				{
+					input: {
+						name: submission.name,
+						unique: {
+							lot: submission.lot,
+							source: submission.source,
+							identifier: submission.identifier,
+						},
+					},
+				},
 			);
 			returnData = { commitMedia: commitMetadata };
 		})
@@ -113,7 +121,16 @@ export const action = async ({ request }: ActionFunctionArgs) => {
 				await serverGqlService.authenticatedRequest(
 					request,
 					CommitMetadataGroupDocument,
-					{ input: submission },
+					{
+						input: {
+							name: submission.name,
+							unique: {
+								lot: submission.lot,
+								source: submission.source,
+								identifier: submission.identifier,
+							},
+						},
+					},
 				);
 			returnData = { commitMetadataGroup };
 		})
@@ -152,7 +169,6 @@ export const action = async ({ request }: ActionFunctionArgs) => {
 			);
 		})
 		.with("addEntityToCollection", async () => {
-			removeCachedUserCollectionsList(request);
 			const [submission] = getChangeCollectionToEntityVariables(formData);
 			const addTo = [submission.collectionName];
 			if (submission.collectionName === "Watchlist") addTo.push("Monitoring");
@@ -179,7 +195,6 @@ export const action = async ({ request }: ActionFunctionArgs) => {
 			);
 		})
 		.with("removeEntityFromCollection", async () => {
-			removeCachedUserCollectionsList(request);
 			const [submission] = getChangeCollectionToEntityVariables(formData);
 			await serverGqlService.authenticatedRequest(
 				request,
@@ -385,7 +400,6 @@ export const action = async ({ request }: ActionFunctionArgs) => {
 					{ input: updates },
 				);
 			await sleepForHalfSecond(request);
-			await removeCachedUserCollectionsList(request);
 			extendResponseHeaders(
 				headers,
 				await createToastHeaders({
@@ -405,7 +419,6 @@ export const action = async ({ request }: ActionFunctionArgs) => {
 				{ input: submission },
 			);
 			await sleepForHalfSecond(request);
-			await removeCachedUserCollectionsList(request);
 			extendResponseHeaders(
 				headers,
 				await createToastHeaders({
@@ -451,7 +464,6 @@ export const action = async ({ request }: ActionFunctionArgs) => {
 					},
 				);
 			}
-			await removeCachedUserCollectionsList(request);
 		})
 		.run();
 	if (redirectTo) {
@@ -462,6 +474,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
 };
 
 const commitMediaSchema = z.object({
+	name: z.string(),
 	identifier: z.string(),
 	lot: z.nativeEnum(MediaLot),
 	source: z.nativeEnum(MediaSource),
diff --git a/crates/background/Cargo.toml b/crates/background/Cargo.toml
deleted file mode 100644
index 03a49d4361..0000000000
--- a/crates/background/Cargo.toml
+++ /dev/null
@@ -1,16 +0,0 @@
-[package]
-name = "background"
-version = "0.1.0"
-edition = "2021"
-
-[dependencies]
-apalis = { workspace = true }
-chrono = { workspace = true }
-chrono-tz = { workspace = true }
-database-models = { path = "../models/database" }
-enums = { path = "../enums" }
-fitness-models = { path = "../models/fitness" }
-media-models = { path = "../models/media" }
-serde = { workspace = true }
-strum = { workspace = true }
-uuid = { workspace = true }
diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs
index e5ec0deebe..aea705423f 100644
--- a/crates/config/src/lib.rs
+++ b/crates/config/src/lib.rs
@@ -304,15 +304,6 @@ pub struct IntegrationConfig {
     pub sync_every_minutes: i32,
 }
 
-#[derive(Debug, Serialize, Deserialize, Clone, Config)]
-#[config(rename_all = "snake_case", env_prefix = "SCHEDULER_")]
-pub struct SchedulerConfig {
-    /// The number of jobs to process every 5 seconds when updating metadata in
-    /// the background.
-    #[setting(default = 5)]
-    pub rate_limit_num: u64,
-}
-
 #[derive(Debug, Serialize, Deserialize, Clone, Config)]
 #[config(rename_all = "snake_case", env_prefix = "SERVER_SMTP_")]
 pub struct SmtpConfig {
@@ -436,9 +427,6 @@ pub struct AppConfig {
     /// Settings related to podcasts.
     #[setting(nested)]
     pub podcasts: PodcastConfig,
-    /// Settings related to scheduler.
-    #[setting(nested)]
-    pub scheduler: SchedulerConfig,
     /// Settings related to server.
     #[setting(nested)]
     pub server: ServerConfig,
diff --git a/crates/migrations/Cargo.toml b/crates/migrations/Cargo.toml
index 08d9ff16b4..248516bb5c 100644
--- a/crates/migrations/Cargo.toml
+++ b/crates/migrations/Cargo.toml
@@ -4,7 +4,7 @@ version = "0.1.0"
 edition = "2021"
 
 [dependencies]
-enums = { path = "../enums" }
+enum-models = { path = "../models/enum" }
 indoc = { workspace = true }
 sea-orm = { workspace = true }
 sea-orm-migration = { workspace = true }
diff --git a/crates/migrations/src/m20230419_create_seen.rs b/crates/migrations/src/m20230419_create_seen.rs
index 283d0bfad1..15e0383217 100644
--- a/crates/migrations/src/m20230419_create_seen.rs
+++ b/crates/migrations/src/m20230419_create_seen.rs
@@ -1,6 +1,6 @@
 use sea_orm_migration::prelude::*;
 
-use enums::SeenState;
+use enum_models::SeenState;
 
 use super::{m20230410_create_metadata::Metadata, m20230417_create_user::User};
 
diff --git a/crates/migrations/src/m20230505_create_review.rs b/crates/migrations/src/m20230505_create_review.rs
index 9648e75970..11362b6dc1 100644
--- a/crates/migrations/src/m20230505_create_review.rs
+++ b/crates/migrations/src/m20230505_create_review.rs
@@ -1,5 +1,5 @@
 // FIXME: Rename this to m20230508_create_review in the next major release
-use enums::Visibility;
+use enum_models::Visibility;
 use indoc::indoc;
 use sea_orm_migration::prelude::*;
 
diff --git a/crates/migrations/src/m20230818_create_workout_template.rs b/crates/migrations/src/m20230818_create_workout_template.rs
index 7e71737b37..13cb716e3f 100644
--- a/crates/migrations/src/m20230818_create_workout_template.rs
+++ b/crates/migrations/src/m20230818_create_workout_template.rs
@@ -1,5 +1,5 @@
 // FIXME: Rename this to m20230506_create_workout_template in the next major release
-use enums::Visibility;
+use enum_models::Visibility;
 use sea_orm_migration::prelude::*;
 
 use super::m20230417_create_user::User;
diff --git a/crates/migrations/src/m20241006_changes_for_issue_1056.rs b/crates/migrations/src/m20241006_changes_for_issue_1056.rs
index 06f11a9b2c..c438d63b1c 100644
--- a/crates/migrations/src/m20241006_changes_for_issue_1056.rs
+++ b/crates/migrations/src/m20241006_changes_for_issue_1056.rs
@@ -1,4 +1,4 @@
-use enums::MediaLot;
+use enum_models::MediaLot;
 use sea_orm_migration::prelude::*;
 
 #[derive(DeriveMigrationName)]
diff --git a/crates/models/background/Cargo.toml b/crates/models/background/Cargo.toml
new file mode 100644
index 0000000000..ff8763a459
--- /dev/null
+++ b/crates/models/background/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "background-models"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+chrono = { workspace = true }
+chrono-tz = { workspace = true }
+database-models = { path = "../database" }
+media-models = { path = "../media" }
+serde = { workspace = true }
+strum = { workspace = true }
+uuid = { workspace = true }
diff --git a/crates/background/src/lib.rs b/crates/models/background/src/lib.rs
similarity index 66%
rename from crates/background/src/lib.rs
rename to crates/models/background/src/lib.rs
index f9f1e1c2df..353a13e632 100644
--- a/crates/background/src/lib.rs
+++ b/crates/models/background/src/lib.rs
@@ -1,43 +1,48 @@
 use chrono::DateTime;
 use chrono_tz::Tz;
 use database_models::seen;
-use enums::{MediaLot, MediaSource};
-use fitness_models::GithubExercise;
 use media_models::{DeployImportJobInput, ProgressUpdateInput, ReviewPostedEvent};
 use serde::{Deserialize, Serialize};
 use strum::Display;
 use uuid::Uuid;
 
-// The background jobs which cannot be throttled.
-#[derive(Debug, Deserialize, Serialize, Display)]
-pub enum CoreApplicationJob {
-    SyncIntegrationsData(String),
+#[derive(Debug, Deserialize, Serialize, Display, Clone)]
+pub enum HpApplicationJob {
     ReviewPosted(ReviewPostedEvent),
+    SyncUserIntegrationsData(String),
+    RecalculateUserActivitiesAndSummary(String, bool),
     BulkProgressUpdate(String, Vec<ProgressUpdateInput>),
 }
 
-// The background jobs which can be deployed by the application.
-#[derive(Debug, Deserialize, Serialize, Display)]
-pub enum ApplicationJob {
+#[derive(Debug, Deserialize, Serialize, Display, Clone)]
+pub enum MpApplicationJob {
     UpdatePerson(String),
     SyncIntegrationsData,
     UpdateExerciseLibrary,
     PerformExport(String),
+    UpdateGithubExercises,
+    UpdateMetadata(String),
     PerformBackgroundTasks,
     RecalculateCalendarEvents,
     ReviseUserWorkouts(String),
     UpdateMetadataGroup(String),
-    UpdateMetadata(String, bool),
+    ImportFromExternalSource(String, Box<DeployImportJobInput>),
+}
+
+#[derive(Debug, Deserialize, Serialize, Display, Clone)]
+pub enum LpApplicationJob {
     HandleOnSeenComplete(String),
     HandleAfterMediaSeenTasks(seen::Model),
-    UpdateGithubExerciseJob(GithubExercise),
     HandleEntityAddedToCollectionEvent(Uuid),
-    RecalculateUserActivitiesAndSummary(String, bool),
-    AssociateGroupWithMetadata(MediaLot, MediaSource, String),
-    ImportFromExternalSource(String, Box<DeployImportJobInput>),
 }
 
-// Cron Jobs
+#[derive(Debug, Deserialize, Serialize, Display, Clone)]
+pub enum ApplicationJob {
+    Lp(LpApplicationJob),
+    Hp(HpApplicationJob),
+    Mp(MpApplicationJob),
+}
+
 pub struct ScheduledJob(pub DateTime<Tz>);
 
 impl From<DateTime<Tz>> for ScheduledJob {
diff --git a/crates/models/common/Cargo.toml b/crates/models/common/Cargo.toml
index 5c8ca4646c..8a15bfa9c0 100644
--- a/crates/models/common/Cargo.toml
+++ b/crates/models/common/Cargo.toml
@@ -7,7 +7,7 @@ edition = "2021"
 async-graphql = { workspace = true }
 chrono = { workspace = true }
 educe = { workspace = true }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 enum_meta = { workspace = true }
 rust_decimal = { workspace = true }
 schematic = { workspace = true }
diff --git a/crates/models/common/src/lib.rs b/crates/models/common/src/lib.rs
index e303a8e54d..1ca49752d3 100644
--- a/crates/models/common/src/lib.rs
+++ b/crates/models/common/src/lib.rs
@@ -2,7 +2,7 @@ use async_graphql::{Enum, InputObject, SimpleObject};
 use chrono::NaiveDate;
 use educe::Educe;
 use enum_meta::{meta, Meta};
-use enums::{EntityLot, MediaLot, MediaSource};
+use enum_models::{EntityLot, MediaLot, MediaSource};
 use rust_decimal::Decimal;
 use schematic::{ConfigEnum, Schematic};
 use sea_orm::{prelude::DateTimeUtc, FromJsonQueryResult};
@@ -324,6 +324,7 @@ pub enum ApplicationCacheKey {
     IgdbSettings,
     TmdbSettings,
     ListennotesSettings,
+    UserCollectionsList(UserLevelCacheKey<()>),
     UserAnalyticsParameters(UserLevelCacheKey<()>),
     PeopleSearch(UserLevelCacheKey<PeopleSearchInput>),
     UserAnalytics(UserLevelCacheKey<UserAnalyticsInput>),
diff --git a/crates/models/database/Cargo.toml b/crates/models/database/Cargo.toml
index eaf5059742..79a1b9fc5e 100644
--- a/crates/models/database/Cargo.toml
+++ b/crates/models/database/Cargo.toml
@@ -12,7 +12,7 @@ boilermates = { workspace = true }
 chrono = { workspace = true }
 common-models = { path = "../../models/common" }
 educe = { workspace = true }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 importer-models = { path = "../../models/importer" }
 file-storage-service = { path = "../../services/file-storage" }
 fitness-models = { path = "../../models/fitness" }
diff --git a/crates/models/database/src/collection_to_entity.rs b/crates/models/database/src/collection_to_entity.rs
index 6389ed746a..25dd24ac23 100644
--- a/crates/models/database/src/collection_to_entity.rs
+++ b/crates/models/database/src/collection_to_entity.rs
@@ -1,6 +1,6 @@
 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
 
-use enums::EntityLot;
+use enum_models::EntityLot;
 use sea_orm::entity::prelude::*;
 use serde::{Deserialize, Serialize};
 use uuid::Uuid;
diff --git a/crates/models/database/src/daily_user_activity.rs b/crates/models/database/src/daily_user_activity.rs
index 6654fbd67f..a5b05ccb02 100644
--- a/crates/models/database/src/daily_user_activity.rs
+++ b/crates/models/database/src/daily_user_activity.rs
@@ -1,7 +1,7 @@
 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
 
 use common_models::DailyUserActivityHourRecord;
-use enums::{ExerciseEquipment, ExerciseMuscle};
+use enum_models::{ExerciseEquipment, ExerciseMuscle};
 use sea_orm::entity::prelude::*;
 use serde::{Deserialize, Serialize};
 
diff --git a/crates/models/database/src/exercise.rs b/crates/models/database/src/exercise.rs
index 51133cb29d..b1a2fc9d59 100644
--- a/crates/models/database/src/exercise.rs
+++ b/crates/models/database/src/exercise.rs
@@ -3,7 +3,7 @@
 use application_utils::GraphqlRepresentation;
 use async_graphql::{InputObject, Result, SimpleObject};
 use async_trait::async_trait;
-use enums::{
+use enum_models::{
     ExerciseEquipment, ExerciseForce, ExerciseLevel, ExerciseLot, ExerciseMechanic, ExerciseMuscle,
     ExerciseSource,
 };
diff --git a/crates/models/database/src/functions.rs b/crates/models/database/src/functions.rs
index 752512ac44..dd2b2dd164 100644
--- a/crates/models/database/src/functions.rs
+++ b/crates/models/database/src/functions.rs
@@ -1,6 +1,6 @@
 use async_graphql::Result;
 use chrono::Utc;
-use enums::EntityLot;
+use enum_models::EntityLot;
 use sea_orm::{
     ActiveModelTrait, ActiveValue, ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter,
 };
diff --git a/crates/models/database/src/import_report.rs b/crates/models/database/src/import_report.rs
index f3883f6547..6713568c26 100644
--- a/crates/models/database/src/import_report.rs
+++ b/crates/models/database/src/import_report.rs
@@ -2,7 +2,7 @@
 
 use async_graphql::SimpleObject;
 use async_trait::async_trait;
-use enums::ImportSource;
+use enum_models::ImportSource;
 use importer_models::ImportResultResponse;
 use nanoid::nanoid;
 use sea_orm::{entity::prelude::*, ActiveValue};
diff --git a/crates/models/database/src/integration.rs b/crates/models/database/src/integration.rs
index b00a911dbe..c396a1c94f 100644
--- a/crates/models/database/src/integration.rs
+++ b/crates/models/database/src/integration.rs
@@ -2,7 +2,7 @@
 
 use async_graphql::{InputObject, SimpleObject};
 use async_trait::async_trait;
-use enums::{IntegrationLot, IntegrationProvider};
+use enum_models::{IntegrationLot, IntegrationProvider};
 use media_models::IntegrationProviderSpecifics;
 use nanoid::nanoid;
 use sea_orm::{entity::prelude::*, ActiveValue};
diff --git a/crates/models/database/src/metadata.rs b/crates/models/database/src/metadata.rs
index 6708bbcb52..d6eb422953 100644
--- a/crates/models/database/src/metadata.rs
+++ b/crates/models/database/src/metadata.rs
@@ -2,7 +2,7 @@
 
 use async_trait::async_trait;
 use chrono::NaiveDate;
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use media_models::{
     AnimeSpecifics, AudioBookSpecifics, BookSpecifics, MangaSpecifics, MetadataExternalIdentifiers,
     MetadataFreeCreator, MetadataImage, MetadataVideo, MovieSpecifics, MusicSpecifics,
diff --git a/crates/models/database/src/metadata_group.rs b/crates/models/database/src/metadata_group.rs
index dfbd9b8f06..e747a3bcb7 100644
--- a/crates/models/database/src/metadata_group.rs
+++ b/crates/models/database/src/metadata_group.rs
@@ -3,7 +3,7 @@
 use async_graphql::SimpleObject;
 use async_trait::async_trait;
 use boilermates::boilermates;
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use media_models::MetadataImage;
 use nanoid::nanoid;
 use sea_orm::{entity::prelude::*, ActiveValue};
diff --git a/crates/models/database/src/metadata_to_metadata.rs b/crates/models/database/src/metadata_to_metadata.rs
index c45b1f9c5c..e62e2d3a43 100644
--- a/crates/models/database/src/metadata_to_metadata.rs
+++ b/crates/models/database/src/metadata_to_metadata.rs
@@ -1,6 +1,6 @@
 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6
 
-use enums::MetadataToMetadataRelation;
+use enum_models::MetadataToMetadataRelation;
 use sea_orm::entity::prelude::*;
 use serde::{Deserialize, Serialize};
 use uuid::Uuid;
diff --git a/crates/models/database/src/monitored_entity.rs b/crates/models/database/src/monitored_entity.rs
index 87950570fe..70369a576e 100644
--- a/crates/models/database/src/monitored_entity.rs
+++ b/crates/models/database/src/monitored_entity.rs
@@ -1,6 +1,6 @@
 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
 
-use enums::EntityLot;
+use enum_models::EntityLot;
 use sea_orm::entity::prelude::*;
 use serde::{Deserialize, Serialize};
 
diff --git a/crates/models/database/src/notification_platform.rs b/crates/models/database/src/notification_platform.rs
index 8d919f0c56..8791e8ff0c 100644
--- a/crates/models/database/src/notification_platform.rs
+++ b/crates/models/database/src/notification_platform.rs
@@ -2,7 +2,7 @@
 
 use async_graphql::SimpleObject;
 use async_trait::async_trait;
-use enums::NotificationPlatformLot;
+use enum_models::NotificationPlatformLot;
 use nanoid::nanoid;
 use sea_orm::{entity::prelude::*, ActiveValue};
 use user_models::NotificationPlatformSpecifics;
diff --git a/crates/models/database/src/person.rs b/crates/models/database/src/person.rs
index 268253ab8b..c453208295 100644
--- a/crates/models/database/src/person.rs
+++ b/crates/models/database/src/person.rs
@@ -4,7 +4,7 @@ use async_graphql::SimpleObject;
 use async_trait::async_trait;
 use chrono::NaiveDate;
 use common_models::PersonSourceSpecifics;
-use enums::MediaSource;
+use enum_models::MediaSource;
 use media_models::{MetadataImage, PersonStateChanges};
 use nanoid::nanoid;
 use sea_orm::{entity::prelude::*, ActiveValue};
diff --git a/crates/models/database/src/review.rs b/crates/models/database/src/review.rs
index 4eb462039b..27f6140fc2 100644
--- a/crates/models/database/src/review.rs
+++ b/crates/models/database/src/review.rs
@@ -1,7 +1,7 @@
 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
 
 use async_trait::async_trait;
-use enums::{EntityLot, Visibility};
+use enum_models::{EntityLot, Visibility};
 use media_models::{
     ImportOrExportItemReviewComment, SeenAnimeExtraInformation, SeenMangaExtraInformation,
     SeenPodcastExtraOptionalInformation, SeenShowExtraOptionalInformation,
diff --git a/crates/models/database/src/seen.rs b/crates/models/database/src/seen.rs
index 088b61025d..970b24c1fe 100644
--- a/crates/models/database/src/seen.rs
+++ b/crates/models/database/src/seen.rs
@@ -4,7 +4,7 @@ use async_graphql::SimpleObject;
 use async_trait::async_trait;
 use chrono::{NaiveDate, Utc};
 use educe::Educe;
-use enums::{EntityLot, SeenState};
+use enum_models::{EntityLot, SeenState};
 use media_models::{
     SeenAnimeExtraInformation, SeenMangaExtraInformation, SeenPodcastExtraInformation,
     SeenShowExtraInformation,
diff --git a/crates/models/database/src/user.rs b/crates/models/database/src/user.rs
index d02e7e6559..220023c5a3 100644
--- a/crates/models/database/src/user.rs
+++ b/crates/models/database/src/user.rs
@@ -6,7 +6,7 @@ use argon2::{
 };
 use async_graphql::SimpleObject;
 use async_trait::async_trait;
-use enums::UserLot;
+use enum_models::UserLot;
 use sea_orm::{entity::prelude::*, ActiveValue};
 use serde::{Deserialize, Serialize};
 use user_models::{UserExtraInformation, UserPreferences};
diff --git a/crates/models/database/src/user_notification.rs b/crates/models/database/src/user_notification.rs
index dc46f661f5..55019cea89 100644
--- a/crates/models/database/src/user_notification.rs
+++ b/crates/models/database/src/user_notification.rs
@@ -1,6 +1,6 @@
 //! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1
 
-use enums::UserNotificationLot;
+use enum_models::UserNotificationLot;
 use sea_orm::entity::prelude::*;
 use uuid::Uuid;
 
diff --git a/crates/models/database/src/user_to_entity.rs b/crates/models/database/src/user_to_entity.rs
index 7fa472d42a..a64758e11b 100644
--- a/crates/models/database/src/user_to_entity.rs
+++ b/crates/models/database/src/user_to_entity.rs
@@ -1,7 +1,7 @@
 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.14
 
 use async_graphql::SimpleObject;
-use enums::UserToMediaReason;
+use enum_models::UserToMediaReason;
 use fitness_models::UserToExerciseExtraInformation;
 use sea_orm::entity::prelude::*;
 use serde::{Deserialize, Serialize};
diff --git a/crates/models/database/src/workout_template.rs b/crates/models/database/src/workout_template.rs
index f97cd06b99..b022d09dff 100644
--- a/crates/models/database/src/workout_template.rs
+++ b/crates/models/database/src/workout_template.rs
@@ -1,7 +1,7 @@
 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
 
 use async_graphql::SimpleObject;
-use enums::Visibility;
+use enum_models::Visibility;
 use fitness_models::{WorkoutInformation, WorkoutSummary};
 use schematic::Schematic;
 use sea_orm::entity::prelude::*;
diff --git a/crates/models/dependent/Cargo.toml b/crates/models/dependent/Cargo.toml
index 100de3ff35..6808eab950 100644
--- a/crates/models/dependent/Cargo.toml
+++ b/crates/models/dependent/Cargo.toml
@@ -9,7 +9,7 @@ chrono = { workspace = true }
 common-models = { path = "../common" }
 config = { path = "../../config" }
 database-models = { path = "../database" }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 importer-models = { path = "../importer" }
 media-models = { path = "../media" }
 fitness-models = { path = "../fitness" }
diff --git a/crates/models/dependent/src/lib.rs b/crates/models/dependent/src/lib.rs
index e70918ad81..ad58488c81 100644
--- a/crates/models/dependent/src/lib.rs
+++ b/crates/models/dependent/src/lib.rs
@@ -11,19 +11,19 @@ use database_models::{
     collection, exercise, metadata, metadata_group, person, seen, user, user_measurement,
     user_to_entity, workout, workout_template,
 };
-use enums::{
+use enum_models::{
     ExerciseEquipment, ExerciseForce, ExerciseLevel, ExerciseLot, ExerciseMechanic, ExerciseMuscle,
     MediaLot, MediaSource, UserToMediaReason, WorkoutSetPersonalBest,
 };
 use fitness_models::{UserToExerciseHistoryExtraInformation, UserWorkoutInput};
 use importer_models::ImportFailedItem;
 use media_models::{
-    CreateOrUpdateCollectionInput, EntityWithLot, GenreListItem, GraphqlMediaAssets,
-    ImportOrExportExerciseItem, ImportOrExportMetadataGroupItem, ImportOrExportMetadataItem,
-    ImportOrExportPersonItem, MetadataCreatorGroupedByRole, MetadataGroupSearchItem,
-    MetadataSearchItemResponse, PeopleSearchItem, PersonDetailsGroupedByRole, ReviewItem,
-    UserDetailsError, UserMediaNextEntry, UserMetadataDetailsEpisodeProgress,
-    UserMetadataDetailsShowSeasonProgress,
+    CollectionItem, CreateOrUpdateCollectionInput, EntityWithLot, GenreListItem,
+    GraphqlMediaAssets, ImportOrExportExerciseItem, ImportOrExportMetadataGroupItem,
+    ImportOrExportMetadataItem, ImportOrExportPersonItem, MetadataCreatorGroupedByRole,
+    MetadataGroupSearchItem, MetadataSearchItemResponse, PeopleSearchItem,
+    PersonDetailsGroupedByRole, ReviewItem, UserDetailsError, UserMediaNextEntry,
+    UserMetadataDetailsEpisodeProgress, UserMetadataDetailsShowSeasonProgress,
 };
 use rust_decimal::Decimal;
 use schematic::Schematic;
@@ -414,6 +414,7 @@ pub struct EmptyCacheValue {
     pub _empty: (),
 }
 
+pub type UserCollectionsListResponse = Vec<CollectionItem>;
 pub type PeopleSearchResponse = SearchResults<PeopleSearchItem>;
 pub type MetadataSearchResponse = SearchResults<MetadataSearchItemResponse>;
 pub type MetadataGroupSearchResponse = SearchResults<MetadataGroupSearchItem>;
@@ -430,5 +431,6 @@ pub enum ApplicationCacheValue {
     MetadataSearch(MetadataSearchResponse),
     ListennotesSettings(ListennotesSettings),
     UserAnalyticsParameters(ApplicationDateRange),
+    UserCollectionsList(UserCollectionsListResponse),
     MetadataGroupSearch(MetadataGroupSearchResponse),
 }
diff --git a/crates/enums/Cargo.toml b/crates/models/enum/Cargo.toml
similarity index 94%
rename from crates/enums/Cargo.toml
rename to crates/models/enum/Cargo.toml
index 90eda1ed39..69ce2706b2 100644
--- a/crates/enums/Cargo.toml
+++ b/crates/models/enum/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "enums"
+name = "enum-models"
 version = "0.1.0"
 edition = "2021"
 
diff --git a/crates/enums/src/lib.rs b/crates/models/enum/src/lib.rs
similarity index 100%
rename from crates/enums/src/lib.rs
rename to crates/models/enum/src/lib.rs
diff --git a/crates/models/fitness/Cargo.toml b/crates/models/fitness/Cargo.toml
index 11ff7e05c3..94bb6238f7 100644
--- a/crates/models/fitness/Cargo.toml
+++ b/crates/models/fitness/Cargo.toml
@@ -8,7 +8,7 @@ async-graphql = { workspace = true }
 common-models = { path = "../../models/common" }
 derive_more = { workspace = true }
 educe = { workspace = true }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 rust_decimal = { workspace = true }
 schematic = { workspace = true }
 sea-orm = { workspace = true }
diff --git a/crates/models/fitness/src/lib.rs b/crates/models/fitness/src/lib.rs
index 4be01d0f01..7a568449b5 100644
--- a/crates/models/fitness/src/lib.rs
+++ b/crates/models/fitness/src/lib.rs
@@ -4,7 +4,7 @@ use async_graphql::{Enum, InputObject, SimpleObject};
 use common_models::{SearchInput, StoredUrl, UpdateComplexJsonInput};
 use derive_more::{Add, AddAssign, Sum};
 use educe::Educe;
-use enums::{
+use enum_models::{
     ExerciseEquipment, ExerciseForce, ExerciseLevel, ExerciseLot, ExerciseMechanic, ExerciseMuscle,
     WorkoutSetPersonalBest,
 };
diff --git a/crates/models/importer/Cargo.toml b/crates/models/importer/Cargo.toml
index 65ddc3e0bd..34b46d5e47 100644
--- a/crates/models/importer/Cargo.toml
+++ b/crates/models/importer/Cargo.toml
@@ -5,7 +5,7 @@ edition = "2021"
 
 [dependencies]
 async-graphql = { workspace = true }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 sea-orm = { workspace = true }
 serde = { workspace = true }
 serde_json = { workspace = true }
diff --git a/crates/models/importer/src/lib.rs b/crates/models/importer/src/lib.rs
index 31c38a2c91..a239c971e7 100644
--- a/crates/models/importer/src/lib.rs
+++ b/crates/models/importer/src/lib.rs
@@ -1,5 +1,5 @@
 use async_graphql::{Enum, SimpleObject};
-use enums::MediaLot;
+use enum_models::MediaLot;
 use sea_orm::FromJsonQueryResult;
 use serde::{Deserialize, Serialize};
 
diff --git a/crates/models/media/Cargo.toml b/crates/models/media/Cargo.toml
index 1c14fc2fd8..b385813706 100644
--- a/crates/models/media/Cargo.toml
+++ b/crates/models/media/Cargo.toml
@@ -9,7 +9,7 @@ boilermates = { workspace = true }
 chrono = { workspace = true }
 common-models = { path = "../../models/common" }
 common-utils = { path = "../../utils/common" }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 rust_decimal = { workspace = true }
 schematic = { workspace = true }
 sea-orm = { workspace = true }
diff --git a/crates/models/media/src/lib.rs b/crates/models/media/src/lib.rs
index e9f29efcf6..c415233a0e 100644
--- a/crates/models/media/src/lib.rs
+++ b/crates/models/media/src/lib.rs
@@ -8,7 +8,7 @@ use common_models::{
     StringIdObject,
 };
 use common_utils::deserialize_date;
-use enums::{
+use enum_models::{
     EntityLot, ImportSource, IntegrationProvider, MediaLot, MediaSource, NotificationPlatformLot,
     SeenState, Visibility,
 };
@@ -481,7 +481,7 @@ pub struct MetadataDetails {
     pub publish_year: Option<i32>,
     pub publish_date: Option<NaiveDate>,
     pub suggestions: Vec<PartialMetadataWithoutId>,
-    pub group_identifiers: Vec<String>,
+    pub groups: Vec<CommitMediaInput>,
     pub provider_rating: Option<Decimal>,
     pub watch_providers: Vec<WatchProvider>,
     pub audio_book_specifics: Option<AudioBookSpecifics>,
@@ -826,19 +826,36 @@ pub struct MetadataGroupSearchItem {
     InputObject,
     Hash,
 )]
-pub struct CommitMediaInput {
+pub struct UniqueMediaIdentifier {
     pub lot: MediaLot,
     pub identifier: String,
     pub source: MediaSource,
-    pub force_update: Option<bool>,
+}
+
+#[skip_serializing_none]
+#[derive(
+    Eq,
+    Hash,
+    Clone,
+    Debug,
+    Default,
+    PartialEq,
+    Serialize,
+    Deserialize,
+    InputObject,
+    FromJsonQueryResult,
+)]
+pub struct CommitMediaInput {
+    pub name: String,
+    pub unique: UniqueMediaIdentifier,
 }
 
 #[derive(
     Debug, Serialize, Deserialize, Clone, FromJsonQueryResult, Eq, PartialEq, Default, Hash,
 )]
 pub struct MediaAssociatedPersonStateChanges {
-    pub media: CommitMediaInput,
     pub role: String,
+    pub media: UniqueMediaIdentifier,
 }
 
 #[derive(Debug, Serialize, Deserialize, Clone, FromJsonQueryResult, Eq, PartialEq, Default)]
@@ -1152,29 +1169,29 @@ pub struct CollectionContentsFilter {
 
 #[derive(Debug, InputObject)]
 pub struct CollectionContentsInput {
+    pub take: Option<u64>,
     pub collection_id: String,
     pub search: Option<SearchInput>,
     pub filter: Option<CollectionContentsFilter>,
-    pub take: Option<u64>,
     pub sort: Option<SortInput<CollectionContentsSortBy>>,
 }
 
-#[derive(Debug, SimpleObject, FromQueryResult)]
+#[derive(Debug, Clone, SimpleObject, FromQueryResult, PartialEq, Eq, Serialize, Deserialize)]
 pub struct CollectionItem {
     pub id: String,
-    pub name: String,
     pub count: i64,
+    pub name: String,
     pub is_default: bool,
-    pub description: Option<String>,
-    pub information_template: Option<Vec<CollectionExtraInformation>>,
     pub creator: IdAndNamedObject,
+    pub description: Option<String>,
     pub collaborators: Vec<IdAndNamedObject>,
+    pub information_template: Option<Vec<CollectionExtraInformation>>,
 }
 
 #[derive(Debug, Serialize, Deserialize, SimpleObject, Clone)]
 pub struct MetadataCreator {
-    pub id: Option<String>,
     pub name: String,
+    pub id: Option<String>,
     pub image: Option<String>,
     pub character: Option<String>,
 }
diff --git a/crates/models/user/Cargo.toml b/crates/models/user/Cargo.toml
index 1c45311efe..6ffdd3edd3 100644
--- a/crates/models/user/Cargo.toml
+++ b/crates/models/user/Cargo.toml
@@ -7,7 +7,7 @@ edition = "2021"
 async-graphql = { workspace = true }
 common-models = { path = "../../models/common" }
 educe = { workspace = true }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 fitness-models = { path = "../../models/fitness" }
 sea-orm = { workspace = true }
 serde = { workspace = true }
diff --git a/crates/models/user/src/lib.rs b/crates/models/user/src/lib.rs
index 673c1aa1cc..55688386c6 100644
--- a/crates/models/user/src/lib.rs
+++ b/crates/models/user/src/lib.rs
@@ -1,7 +1,7 @@
 use async_graphql::{Enum, InputObject, SimpleObject};
 use common_models::MediaStateChanged;
 use educe::Educe;
-use enums::{MediaLot, UserLot};
+use enum_models::{MediaLot, UserLot};
 use fitness_models::{SetRestTimersSettings, UserUnitSystem};
 use sea_orm::{FromJsonQueryResult, Iterable};
 use serde::{Deserialize, Serialize};
diff --git a/crates/providers/Cargo.toml b/crates/providers/Cargo.toml
index 131511b041..b2b5de12e6 100644
--- a/crates/providers/Cargo.toml
+++ b/crates/providers/Cargo.toml
@@ -16,7 +16,7 @@ convert_case = { workspace = true }
 database-models = { path = "../models/database" }
 dependent-models = { path = "../models/dependent" }
 educe = { workspace = true }
-enums = { path = "../enums" }
+enum-models = { path = "../models/enum" }
 graphql_client = { workspace = true }
 hashbag = { workspace = true }
 itertools = { workspace = true }
diff --git a/crates/providers/src/anilist/mod.rs b/crates/providers/src/anilist/mod.rs
index cb7eeb9dd5..4fd89c2c94 100644
--- a/crates/providers/src/anilist/mod.rs
+++ b/crates/providers/src/anilist/mod.rs
@@ -6,7 +6,7 @@ use common_models::{PersonSourceSpecifics, SearchDetails, StoredUrl};
 use common_utils::PAGE_SIZE;
 use config::AnilistPreferredLanguage;
 use dependent_models::{PeopleSearchResponse, SearchResults};
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use graphql_client::{GraphQLQuery, Response};
 use itertools::Itertools;
 use media_models::{
@@ -718,7 +718,7 @@ async fn media_details(
         manga_specifics,
         suggestions,
         provider_rating: score,
-        group_identifiers: vec![],
+        groups: vec![],
         s3_images: vec![],
         production_status: media_status_string(media.status),
         original_language: None,
diff --git a/crates/providers/src/audible.rs b/crates/providers/src/audible.rs
index 3e4ebde88d..1995e471a8 100644
--- a/crates/providers/src/audible.rs
+++ b/crates/providers/src/audible.rs
@@ -7,12 +7,12 @@ use convert_case::{Case, Casing};
 use database_models::metadata_group::MetadataGroupWithoutId;
 use dependent_models::{PeopleSearchResponse, SearchResults};
 use educe::Educe;
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use itertools::Itertools;
 use media_models::{
-    AudioBookSpecifics, MetadataDetails, MetadataFreeCreator, MetadataImageForMediaDetails,
-    MetadataPerson, MetadataSearchItem, PartialMetadataPerson, PartialMetadataWithoutId,
-    PeopleSearchItem,
+    AudioBookSpecifics, CommitMediaInput, MetadataDetails, MetadataFreeCreator,
+    MetadataImageForMediaDetails, MetadataPerson, MetadataSearchItem, PartialMetadataPerson,
+    PartialMetadataWithoutId, PeopleSearchItem, UniqueMediaIdentifier,
 };
 use paginate::Pages;
 use reqwest::Client;
@@ -316,12 +316,19 @@ impl MediaProvider for AudibleService {
             .await
             .map_err(|e| anyhow!(e))?;
         let data: AudibleItemResponse = rsp.json().await.map_err(|e| anyhow!(e))?;
+        let mut item = self.audible_response_to_search_response(data.product.clone());
+        let mut suggestions = vec![];
         let mut groups = vec![];
-        for s in data.product.clone().series.unwrap_or_default() {
-            groups.push(s.asin);
+        for s in data.product.series.unwrap_or_default() {
+            groups.push(CommitMediaInput {
+                name: s.title,
+                unique: UniqueMediaIdentifier {
+                    lot: item.lot,
+                    identifier: s.asin,
+                    source: MediaSource::Audible,
+                },
+            });
         }
-        let mut item = self.audible_response_to_search_response(data.product);
-        let mut suggestions = vec![];
         for sim_type in AudibleSimilarityType::iter() {
             let data: AudibleItemSimResponse = self
                 .client
@@ -348,7 +355,7 @@ impl MediaProvider for AudibleService {
             }
         }
         item.suggestions = suggestions.into_iter().unique().collect();
-        item.group_identifiers = groups;
+        item.groups = groups;
         Ok(item)
     }
 
diff --git a/crates/providers/src/google_books.rs b/crates/providers/src/google_books.rs
index 8d016ffdfb..a5a3ee1c8c 100644
--- a/crates/providers/src/google_books.rs
+++ b/crates/providers/src/google_books.rs
@@ -5,7 +5,7 @@ use common_models::SearchDetails;
 use common_utils::{convert_date_to_year, PAGE_SIZE};
 use convert_case::{Case, Casing};
 use dependent_models::SearchResults;
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use itertools::Itertools;
 use media_models::{
     BookSpecifics, MetadataDetails, MetadataFreeCreator, MetadataImageForMediaDetails,
diff --git a/crates/providers/src/igdb.rs b/crates/providers/src/igdb.rs
index 543313b4a5..8aaa92ffff 100644
--- a/crates/providers/src/igdb.rs
+++ b/crates/providers/src/igdb.rs
@@ -13,12 +13,13 @@ use dependent_models::{
     ApplicationCacheValue, IgdbSettings, MetadataGroupSearchResponse, PeopleSearchResponse,
     SearchResults,
 };
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use itertools::Itertools;
 use media_models::{
-    MetadataDetails, MetadataGroupSearchItem, MetadataImageForMediaDetails, MetadataPerson,
-    MetadataPersonRelated, MetadataSearchItem, MetadataVideo, MetadataVideoSource,
-    PartialMetadataPerson, PartialMetadataWithoutId, PeopleSearchItem, VideoGameSpecifics,
+    CommitMediaInput, MetadataDetails, MetadataGroupSearchItem, MetadataImageForMediaDetails,
+    MetadataPerson, MetadataPersonRelated, MetadataSearchItem, MetadataVideo, MetadataVideoSource,
+    PartialMetadataPerson, PartialMetadataWithoutId, PeopleSearchItem, UniqueMediaIdentifier,
+    VideoGameSpecifics,
 };
 use reqwest::{
     header::{HeaderName, HeaderValue, AUTHORIZATION},
@@ -434,11 +435,18 @@ where id = {id};
         let mut details: Vec<IgdbItemResponse> = rsp.json().await.map_err(|e| anyhow!(e))?;
         let detail = details.pop().ok_or_else(|| anyhow!("No details found"))?;
         let groups = match detail.collection.as_ref() {
-            Some(c) => vec![c.id.to_string()],
+            Some(c) => vec![CommitMediaInput {
+                name: "Loading...".to_string(),
+                unique: UniqueMediaIdentifier {
+                    lot: MediaLot::VideoGame,
+                    source: MediaSource::Igdb,
+                    identifier: c.id.to_string(),
+                },
+            }],
             None => vec![],
         };
         let mut game_details = self.igdb_response_to_search_response(detail);
-        game_details.group_identifiers = groups;
+        game_details.groups = groups;
         Ok(game_details)
     }
 
diff --git a/crates/providers/src/itunes.rs b/crates/providers/src/itunes.rs
index 2b9115dc42..68399991fb 100644
--- a/crates/providers/src/itunes.rs
+++ b/crates/providers/src/itunes.rs
@@ -5,7 +5,7 @@ use chrono::Datelike;
 use common_models::{NamedObject, SearchDetails};
 use common_utils::PAGE_SIZE;
 use dependent_models::SearchResults;
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use itertools::Itertools;
 use media_models::{
     MetadataDetails, MetadataFreeCreator, MetadataImageForMediaDetails, MetadataSearchItem,
diff --git a/crates/providers/src/listennotes.rs b/crates/providers/src/listennotes.rs
index 62a7e6cc5a..3a8206d104 100644
--- a/crates/providers/src/listennotes.rs
+++ b/crates/providers/src/listennotes.rs
@@ -7,7 +7,7 @@ use chrono::Datelike;
 use common_models::{ApplicationCacheKey, SearchDetails};
 use common_utils::{convert_naive_to_utc, PAGE_SIZE};
 use dependent_models::{ApplicationCacheValue, ListennotesSettings, SearchResults};
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use itertools::Itertools;
 use media_models::{
     MetadataDetails, MetadataFreeCreator, MetadataImageForMediaDetails, MetadataSearchItem,
diff --git a/crates/providers/src/mal.rs b/crates/providers/src/mal.rs
index 76e31523db..0c373109e2 100644
--- a/crates/providers/src/mal.rs
+++ b/crates/providers/src/mal.rs
@@ -5,7 +5,7 @@ use common_models::{NamedObject, SearchDetails};
 use common_utils::{convert_date_to_year, convert_string_to_date, PAGE_SIZE};
 use convert_case::{Case, Casing};
 use dependent_models::SearchResults;
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use media_models::{
     AnimeSpecifics, MangaSpecifics, MetadataDetails, MetadataImageForMediaDetails,
     MetadataSearchItem, PartialMetadataWithoutId,
diff --git a/crates/providers/src/manga_updates.rs b/crates/providers/src/manga_updates.rs
index 11ef20c704..0568977d71 100644
--- a/crates/providers/src/manga_updates.rs
+++ b/crates/providers/src/manga_updates.rs
@@ -5,7 +5,7 @@ use chrono::NaiveDate;
 use common_models::{PersonSourceSpecifics, SearchDetails};
 use common_utils::PAGE_SIZE;
 use dependent_models::{PeopleSearchResponse, SearchResults};
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use itertools::Itertools;
 use media_models::{
     MangaSpecifics, MetadataDetails, MetadataImageForMediaDetails, MetadataPerson,
diff --git a/crates/providers/src/openlibrary.rs b/crates/providers/src/openlibrary.rs
index 231b1e4c7c..18a785c6ac 100644
--- a/crates/providers/src/openlibrary.rs
+++ b/crates/providers/src/openlibrary.rs
@@ -6,7 +6,7 @@ use common_models::{PersonSourceSpecifics, SearchDetails};
 use common_utils::{ryot_log, PAGE_SIZE};
 use convert_case::{Case, Casing};
 use dependent_models::{PeopleSearchResponse, SearchResults};
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use itertools::Itertools;
 use media_models::{
     BookSpecifics, MetadataDetails, MetadataImageForMediaDetails, MetadataPerson,
diff --git a/crates/providers/src/tmdb.rs b/crates/providers/src/tmdb.rs
index 7f182af2c8..3e8f7d9e20 100644
--- a/crates/providers/src/tmdb.rs
+++ b/crates/providers/src/tmdb.rs
@@ -4,7 +4,7 @@ use std::{
 };
 
 use anyhow::{anyhow, Result};
-use application_utils::{get_base_http_client, get_current_date};
+use application_utils::get_base_http_client;
 use async_trait::async_trait;
 use chrono::NaiveDate;
 use common_models::{
@@ -16,15 +16,15 @@ use dependent_models::{
     ApplicationCacheValue, MetadataGroupSearchResponse, PeopleSearchResponse, SearchResults,
     TmdbLanguage, TmdbSettings,
 };
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use hashbag::HashBag;
 use itertools::Itertools;
 use media_models::{
-    MetadataDetails, MetadataExternalIdentifiers, MetadataGroupSearchItem, MetadataImage,
-    MetadataImageForMediaDetails, MetadataPerson, MetadataPersonRelated, MetadataSearchItem,
-    MetadataVideo, MetadataVideoSource, MovieSpecifics, PartialMetadataPerson,
+    CommitMediaInput, MetadataDetails, MetadataExternalIdentifiers, MetadataGroupSearchItem,
+    MetadataImage, MetadataImageForMediaDetails, MetadataPerson, MetadataPersonRelated,
+    MetadataSearchItem, MetadataVideo, MetadataVideoSource, MovieSpecifics, PartialMetadataPerson,
     PartialMetadataWithoutId, PeopleSearchItem, ShowEpisode, ShowSeason, ShowSpecifics,
-    WatchProvider,
+    UniqueMediaIdentifier, WatchProvider,
 };
 use reqwest::{
     header::{HeaderValue, AUTHORIZATION},
@@ -32,7 +32,6 @@ use reqwest::{
 };
 use rust_decimal::Decimal;
 use rust_decimal_macros::dec;
-use sea_orm::prelude::DateTimeUtc;
 use serde::{Deserialize, Serialize};
 use serde_json::json;
 use supporting_service::SupportingService;
@@ -346,34 +345,6 @@ impl TmdbService {
         }
     }
 
-    async fn metadata_updated_since(
-        &self,
-        identifier: &str,
-        since: DateTimeUtc,
-        type_: &str,
-    ) -> Result<bool> {
-        #[derive(Debug, Serialize, Deserialize, Clone)]
-        struct TmdbChangesResponse {
-            changes: Vec<serde_json::Value>,
-        }
-        let end_date = get_current_date(&self.supporting_service.timezone);
-        let start_date = since.date_naive();
-        let changes = self
-            .client
-            .get(format!("{}/{}/{}/changes", URL, type_, identifier))
-            .query(&json!({
-                "start_date": start_date,
-                "end_date": end_date,
-            }))
-            .send()
-            .await
-            .map_err(|e| anyhow!(e))?
-            .json::<TmdbChangesResponse>()
-            .await
-            .map_err(|e| anyhow!(e))?;
-        Ok(!changes.changes.is_empty())
-    }
-
     async fn get_external_identifiers(
         &self,
         type_: &str,
@@ -833,20 +804,21 @@ impl MediaProvider for TmdbMovieService {
                 .vote_average
                 .filter(|&av| av != dec!(0))
                 .map(|av| av * dec!(10)),
-            group_identifiers: Vec::from_iter(data.belongs_to_collection)
+            groups: Vec::from_iter(data.belongs_to_collection)
                 .into_iter()
-                .map(|c| c.id.to_string())
+                .map(|c| CommitMediaInput {
+                    name: "Loading...".to_string(),
+                    unique: UniqueMediaIdentifier {
+                        lot: MediaLot::Movie,
+                        source: MediaSource::Tmdb,
+                        identifier: c.id.to_string(),
+                    },
+                })
                 .collect(),
             ..Default::default()
         })
     }
 
-    async fn metadata_updated_since(&self, identifier: &str, since: DateTimeUtc) -> Result<bool> {
-        self.base
-            .metadata_updated_since(identifier, since, "movie")
-            .await
-    }
-
     async fn metadata_group_search(
         &self,
         query: &str,
@@ -1245,12 +1217,6 @@ impl MediaProvider for TmdbShowService {
         })
     }
 
-    async fn metadata_updated_since(&self, identifier: &str, since: DateTimeUtc) -> Result<bool> {
-        self.base
-            .metadata_updated_since(identifier, since, "tv")
-            .await
-    }
-
     async fn metadata_search(
         &self,
         query: &str,
diff --git a/crates/providers/src/vndb.rs b/crates/providers/src/vndb.rs
index 23cb9ba537..fb9e58469d 100644
--- a/crates/providers/src/vndb.rs
+++ b/crates/providers/src/vndb.rs
@@ -4,7 +4,7 @@ use async_trait::async_trait;
 use common_models::{NamedObject, PersonSourceSpecifics, SearchDetails};
 use common_utils::{convert_date_to_year, convert_string_to_date, PAGE_SIZE};
 use dependent_models::{PeopleSearchResponse, SearchResults};
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use itertools::Itertools;
 use media_models::{
     MetadataDetails, MetadataImageForMediaDetails, MetadataPerson, MetadataSearchItem,
diff --git a/crates/providers/src/youtube_music.rs b/crates/providers/src/youtube_music.rs
index 48a003a9c3..0b258b3703 100644
--- a/crates/providers/src/youtube_music.rs
+++ b/crates/providers/src/youtube_music.rs
@@ -4,12 +4,13 @@ use common_models::{PersonSourceSpecifics, SearchDetails, StoredUrl};
 use common_utils::TEMP_DIR;
 use database_models::metadata_group::MetadataGroupWithoutId;
 use dependent_models::{MetadataGroupSearchResponse, PeopleSearchResponse, SearchResults};
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use itertools::Itertools;
 use media_models::{
-    MetadataDetails, MetadataGroupSearchItem, MetadataImage, MetadataImageForMediaDetails,
-    MetadataPerson, MetadataPersonRelated, MetadataSearchItem, MusicSpecifics,
-    PartialMetadataPerson, PartialMetadataWithoutId, PeopleSearchItem,
+    CommitMediaInput, MetadataDetails, MetadataGroupSearchItem, MetadataImage,
+    MetadataImageForMediaDetails, MetadataPerson, MetadataPersonRelated, MetadataSearchItem,
+    MusicSpecifics, PartialMetadataPerson, PartialMetadataWithoutId, PeopleSearchItem,
+    UniqueMediaIdentifier,
 };
 use rustypipe::{
     client::{RustyPipe, RustyPipeQuery},
@@ -73,7 +74,19 @@ impl MediaProvider for YoutubeMusicService {
             title: details.track.name,
             identifier: identifier.clone(),
             source: MediaSource::YoutubeMusic,
-            group_identifiers: details.track.album.into_iter().map(|a| a.id).collect(),
+            groups: details
+                .track
+                .album
+                .into_iter()
+                .map(|a| CommitMediaInput {
+                    name: a.name,
+                    unique: UniqueMediaIdentifier {
+                        identifier: a.id,
+                        lot: MediaLot::Music,
+                        source: MediaSource::YoutubeMusic,
+                    },
+                })
+                .collect(),
             source_url: Some(format!("https://music.youtube.com/watch?v={}", identifier)),
             music_specifics: Some(MusicSpecifics {
                 by_various_artists: Some(details.track.by_va),
diff --git a/crates/resolvers/collection/src/lib.rs b/crates/resolvers/collection/src/lib.rs
index d683aa9dc9..eab96d4a50 100644
--- a/crates/resolvers/collection/src/lib.rs
+++ b/crates/resolvers/collection/src/lib.rs
@@ -3,8 +3,8 @@ use std::sync::Arc;
 use async_graphql::{Context, Object, Result};
 use collection_service::CollectionService;
 use common_models::{ChangeCollectionToEntityInput, StringIdObject};
-use dependent_models::CollectionContents;
-use media_models::{CollectionContentsInput, CollectionItem, CreateOrUpdateCollectionInput};
+use dependent_models::{CollectionContents, UserCollectionsListResponse};
+use media_models::{CollectionContentsInput, CreateOrUpdateCollectionInput};
 use traits::AuthProvider;
 
 #[derive(Default)]
@@ -19,7 +19,7 @@ impl CollectionQuery {
         &self,
         gql_ctx: &Context<'_>,
         name: Option<String>,
-    ) -> Result<Vec<CollectionItem>> {
+    ) -> Result<UserCollectionsListResponse> {
         let service = gql_ctx.data_unchecked::<Arc<CollectionService>>();
         let user_id = self.user_id_from_ctx(gql_ctx).await?;
         service.user_collections_list(&user_id, name).await
diff --git a/crates/resolvers/miscellaneous/src/lib.rs b/crates/resolvers/miscellaneous/src/lib.rs
index d3d32da469..53a5892a16 100644
--- a/crates/resolvers/miscellaneous/src/lib.rs
+++ b/crates/resolvers/miscellaneous/src/lib.rs
@@ -290,7 +290,7 @@ impl MiscellaneousMutation {
         metadata_id: String,
     ) -> Result<bool> {
         let service = gql_ctx.data_unchecked::<Arc<MiscellaneousService>>();
-        service.deploy_update_metadata_job(&metadata_id, true).await
+        service.deploy_update_metadata_job(&metadata_id).await
     }
 
     /// Deploy a job to update a person's metadata.
@@ -349,10 +349,7 @@ impl MiscellaneousMutation {
         input: CommitMediaInput,
     ) -> Result<StringIdObject> {
         let service = gql_ctx.data_unchecked::<Arc<MiscellaneousService>>();
-        service
-            .commit_metadata(input)
-            .await
-            .map(|m| StringIdObject { id: m.id })
+        service.commit_metadata(input).await
     }
 
     /// Fetches details about a person and creates a person item in the database.
diff --git a/crates/services/cache/src/lib.rs b/crates/services/cache/src/lib.rs
index 79452cbb93..16de70bd00 100644
--- a/crates/services/cache/src/lib.rs
+++ b/crates/services/cache/src/lib.rs
@@ -33,14 +33,15 @@ impl CacheService {
             | ApplicationCacheKey::TmdbSettings => None,
 
             ApplicationCacheKey::CoreDetails
-            | ApplicationCacheKey::MetadataRecentlyConsumed { .. }
-            | ApplicationCacheKey::MetadataSearch { .. }
             | ApplicationCacheKey::PeopleSearch { .. }
-            | ApplicationCacheKey::MetadataGroupSearch { .. } => Some(1),
+            | ApplicationCacheKey::MetadataSearch { .. }
+            | ApplicationCacheKey::MetadataGroupSearch { .. }
+            | ApplicationCacheKey::MetadataRecentlyConsumed { .. } => Some(1),
 
             ApplicationCacheKey::UserAnalytics { .. } => Some(2),
 
-            ApplicationCacheKey::UserAnalyticsParameters { .. } => Some(8),
+            ApplicationCacheKey::UserCollectionsList { .. }
+            | ApplicationCacheKey::UserAnalyticsParameters { .. } => Some(8),
 
             ApplicationCacheKey::ProgressUpdateCache { .. } => {
                 Some(self.config.server.progress_update_threshold)
diff --git a/crates/services/collection/Cargo.toml b/crates/services/collection/Cargo.toml
index c9c7a9dece..64326633e8 100644
--- a/crates/services/collection/Cargo.toml
+++ b/crates/services/collection/Cargo.toml
@@ -12,7 +12,7 @@ database-models = { path = "../../models/database" }
 database-utils = { path = "../../utils/database" }
 dependent-utils = { path = "../../utils/dependent" }
 dependent-models = { path = "../../models/dependent" }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 media-models = { path = "../../models/media" }
 migrations = { path = "../../migrations" }
 sea-orm = { workspace = true }
diff --git a/crates/services/collection/src/lib.rs b/crates/services/collection/src/lib.rs
index fb93aaae98..7f90a7a064 100644
--- a/crates/services/collection/src/lib.rs
+++ b/crates/services/collection/src/lib.rs
@@ -3,7 +3,8 @@ use std::sync::Arc;
 use application_utils::graphql_to_db_order;
 use async_graphql::{Error, Result};
 use common_models::{
-    ChangeCollectionToEntityInput, DefaultCollection, SearchDetails, StringIdObject,
+    ApplicationCacheKey, ChangeCollectionToEntityInput, DefaultCollection, SearchDetails,
+    StringIdObject, UserLevelCacheKey,
 };
 use common_utils::PAGE_SIZE;
 use database_models::{
@@ -14,12 +15,15 @@ use database_models::{
     },
     user_to_entity,
 };
-use database_utils::{
-    create_or_update_collection, ilike_sql, item_reviews, remove_entity_from_collection,
+use database_utils::{ilike_sql, item_reviews};
+use dependent_models::{
+    ApplicationCacheValue, CollectionContents, SearchResults, UserCollectionsListResponse,
 };
-use dependent_models::{CollectionContents, SearchResults};
-use dependent_utils::add_entity_to_collection;
-use enums::EntityLot;
+use dependent_utils::{
+    add_entity_to_collection, create_or_update_collection, expire_user_collections_list_cache,
+    remove_entity_from_collection,
+};
+use enum_models::EntityLot;
 use media_models::{
     CollectionContentsInput, CollectionContentsSortBy, CollectionItem,
     CreateOrUpdateCollectionInput, EntityWithLot,
@@ -44,7 +48,15 @@ impl CollectionService {
         &self,
         user_id: &String,
         name: Option<String>,
-    ) -> Result<Vec<CollectionItem>> {
+    ) -> Result<UserCollectionsListResponse> {
+        let cc = &self.0.cache_service;
+        let cache_key = ApplicationCacheKey::UserCollectionsList(UserLevelCacheKey {
+            input: (),
+            user_id: user_id.to_owned(),
+        });
+        if let Some(cached) = cc.get_value(cache_key.clone()).await {
+            return Ok(cached);
+        }
         let user_jsonb_build_object = PgFunc::json_build_object(vec![
             (
                 Expr::val("id"),
@@ -90,7 +102,7 @@ impl CollectionService {
                 )),
             )
             .to_owned();
-        let collections = Collection::find()
+        let response = Collection::find()
             .apply_if(name, |query, v| {
                 query.filter(collection::Column::Name.eq(v))
             })
@@ -128,7 +140,12 @@ impl CollectionService {
             .all(&self.0.db)
             .await
             .unwrap();
-        Ok(collections)
+        cc.set_key(
+            cache_key,
+            ApplicationCacheValue::UserCollectionsList(response.clone()),
+        )
+        .await?;
+        Ok(response)
     }
 
     pub async fn collection_contents(
@@ -283,7 +300,7 @@ impl CollectionService {
         user_id: &String,
         input: CreateOrUpdateCollectionInput,
     ) -> Result<StringIdObject> {
-        create_or_update_collection(&self.0.db, user_id, input).await
+        create_or_update_collection(user_id, input, &self.0).await
     }
 
     pub async fn delete_collection(&self, user_id: String, name: &str) -> Result<bool> {
@@ -299,6 +316,7 @@ impl CollectionService {
             return Ok(false);
         };
         let resp = Collection::delete_by_id(c.id).exec(&self.0.db).await;
+        expire_user_collections_list_cache(&user_id, &self.0).await?;
         Ok(resp.is_ok())
     }
 
@@ -315,6 +333,6 @@ impl CollectionService {
         user_id: &String,
         input: ChangeCollectionToEntityInput,
     ) -> Result<StringIdObject> {
-        remove_entity_from_collection(&self.0.db, user_id, input).await
+        remove_entity_from_collection(user_id, input, &self.0).await
     }
 }
diff --git a/crates/services/exporter/Cargo.toml b/crates/services/exporter/Cargo.toml
index 1ab50c39f4..9a07c81831 100644
--- a/crates/services/exporter/Cargo.toml
+++ b/crates/services/exporter/Cargo.toml
@@ -5,14 +5,14 @@ edition = "2021"
 
 [dependencies]
 async-graphql = { workspace = true }
-background = { path = "../../background" }
+background-models = { path = "../../models/background" }
 chrono = { workspace = true }
 common-models = { path = "../../models/common" }
 common-utils = { path = "../../utils/common" }
 database-models = { path = "../../models/database" }
 database-utils = { path = "../../utils/database" }
 dependent-models = { path = "../../models/dependent" }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 itertools = { workspace = true }
 fitness-models = { path = "../../models/fitness" }
 media-models = { path = "../../models/media" }
diff --git a/crates/services/exporter/src/lib.rs b/crates/services/exporter/src/lib.rs
index eeeed08dd1..ed464ff4cb 100644
--- a/crates/services/exporter/src/lib.rs
+++ b/crates/services/exporter/src/lib.rs
@@ -1,7 +1,7 @@
 use std::{collections::HashMap, fs::File as StdFile, path::PathBuf, sync::Arc};
 
 use async_graphql::{Error, Result};
-use background::ApplicationJob;
+use background_models::{ApplicationJob, MpApplicationJob};
 use chrono::{DateTime, Utc};
 use common_models::ExportJob;
 use common_utils::TEMP_DIR;
@@ -17,7 +17,7 @@ use database_utils::{
     user_workout_template_details,
 };
 use dependent_models::{ImportOrExportWorkoutItem, ImportOrExportWorkoutTemplateItem};
-use enums::EntityLot;
+use enum_models::EntityLot;
 use fitness_models::UserMeasurementsListInput;
 use itertools::Itertools;
 use media_models::{
@@ -56,7 +56,9 @@ pub struct ExporterService(pub Arc<SupportingService>);
 impl ExporterService {
     pub async fn deploy_export_job(&self, user_id: String) -> Result<bool> {
         self.0
-            .perform_application_job(ApplicationJob::PerformExport(user_id))
+            .perform_application_job(ApplicationJob::Mp(
+                MpApplicationJob::PerformExport(user_id),
+            ))
             .await?;
         Ok(true)
     }
@@ -100,7 +102,7 @@ impl ExporterService {
         Ok(resp)
     }
 
-    pub async fn perform_export(&self, user_id: String) -> Result<bool> {
+    pub async fn perform_export(&self, user_id: String) -> Result<()> {
         if !self.0.config.file_storage.is_enabled() {
             return Err(Error::new(
                 "File storage needs to be enabled to perform an export.",
@@ -164,7 +166,7 @@ impl ExporterService {
             .send()
             .await
             .unwrap();
-        Ok(true)
+        Ok(())
     }
 
     async fn export_media(
diff --git a/crates/services/fitness/Cargo.toml b/crates/services/fitness/Cargo.toml
index a6d75e96ab..baf62f1e70 100644
--- a/crates/services/fitness/Cargo.toml
+++ b/crates/services/fitness/Cargo.toml
@@ -6,7 +6,7 @@ edition = "2021"
 [dependencies]
 application-utils = { path = "../../utils/application" }
 async-graphql = { workspace = true }
-background = { path = "../../background" }
+background-models = { path = "../../models/background" }
 common-models = { path = "../../models/common" }
 common-utils = { path = "../../utils/common" }
 const-str = { workspace = true }
@@ -14,7 +14,7 @@ database-models = { path = "../../models/database" }
 database-utils = { path = "../../utils/database" }
 dependent-models = { path = "../../models/dependent" }
 dependent-utils = { path = "../../utils/dependent" }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 fitness-models = { path = "../../models/fitness" }
 migrations = { path = "../../migrations" }
 nanoid = { workspace = true }
diff --git a/crates/services/fitness/src/lib.rs b/crates/services/fitness/src/lib.rs
index d643382a95..89fb11155d 100644
--- a/crates/services/fitness/src/lib.rs
+++ b/crates/services/fitness/src/lib.rs
@@ -2,7 +2,7 @@ use std::sync::Arc;
 
 use application_utils::GraphqlRepresentation;
 use async_graphql::{Error, Result};
-use background::ApplicationJob;
+use background_models::{ApplicationJob, MpApplicationJob};
 use common_models::{SearchDetails, SearchInput, StoredUrl};
 use common_utils::{ryot_log, PAGE_SIZE};
 use database_models::{
@@ -25,7 +25,7 @@ use dependent_utils::{
     create_custom_exercise, create_or_update_user_workout, create_user_measurement,
     db_workout_to_workout_input, get_focused_workout_summary,
 };
-use enums::{EntityLot, ExerciseLot, ExerciseSource, Visibility};
+use enum_models::{EntityLot, ExerciseLot, ExerciseSource, Visibility};
 use fitness_models::{
     ExerciseAttributes, ExerciseCategory, ExerciseListItem, ExerciseSortBy, ExercisesListInput,
     GithubExercise, GithubExerciseAttributes, ProcessedExercise, UpdateUserExerciseSettings,
@@ -411,24 +411,31 @@ impl FitnessService {
         })
     }
 
-    pub async fn deploy_update_exercise_library_job(&self) -> Result<bool> {
+    pub async fn deploy_update_exercise_library_job(&self) -> Result<()> {
         if Exercise::find().count(&self.0.db).await? > 0 {
-            return Ok(true);
+            return Ok(());
         }
         ryot_log!(
             info,
             "Instance does not have exercises data. Deploying job to download them..."
         );
+        self.0
+            .perform_application_job(ApplicationJob::Mp(
+                MpApplicationJob::UpdateGithubExercises,
+            ))
+            .await?;
+        Ok(())
+    }
+
+    pub async fn update_github_exercises(&self) -> Result<()> {
         let exercises = self.get_all_exercises_from_dataset().await?;
         for exercise in exercises {
-            self.0
-                .perform_application_job(ApplicationJob::UpdateGithubExerciseJob(exercise))
-                .await?;
+            self.update_github_exercise(exercise).await?;
         }
-        Ok(true)
+        Ok(())
     }
 
-    pub async fn update_github_exercise(&self, ex: GithubExercise) -> Result<()> {
+    async fn update_github_exercise(&self, ex: GithubExercise) -> Result<()> {
         let attributes = ExerciseAttributes {
             instructions: ex.attributes.instructions,
             internal_images: ex
@@ -511,12 +518,10 @@ impl FitnessService {
         user_id: String,
         timestamp: DateTimeUtc,
     ) -> Result<bool> {
-        let Some(m) = UserMeasurement::find_by_id((timestamp, user_id))
+        let m = UserMeasurement::find_by_id((timestamp, user_id))
             .one(&self.0.db)
             .await?
-        else {
-            return Ok(false);
-        };
+            .ok_or_else(|| Error::new("Measurement does not exist"))?;
         m.delete(&self.0.db).await?;
         Ok(true)
     }
@@ -526,8 +531,7 @@ impl FitnessService {
         user_id: &String,
         input: UserWorkoutInput,
     ) -> Result<String> {
-        let identifier = create_or_update_user_workout(input, user_id, &self.0).await?;
-        Ok(identifier)
+        create_or_update_user_workout(input, user_id, &self.0).await
     }
 
     pub async fn update_user_workout_attributes(
diff --git a/crates/services/importer/Cargo.toml b/crates/services/importer/Cargo.toml
index a6e7e0db59..2f227b97e1 100644
--- a/crates/services/importer/Cargo.toml
+++ b/crates/services/importer/Cargo.toml
@@ -7,7 +7,7 @@ edition = "2021"
 anyhow = { workspace = true }
 application-utils = { path = "../../utils/application" }
 async-graphql = { workspace = true }
-background = { path = "../../background" }
+background-models = { path = "../../models/background" }
 chrono = { workspace = true }
 chrono-tz = { workspace = true }
 convert_case = { workspace = true }
@@ -21,7 +21,7 @@ common-utils = { path = "../../utils/common" }
 database-models = { path = "../../models/database" }
 dependent-models = { path = "../../models/dependent" }
 dependent-utils = { path = "../../utils/dependent" }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 fitness-models = { path = "../../models/fitness" }
 flate2 = { workspace = true }
 importer-models = { path = "../../models/importer" }
@@ -39,6 +39,7 @@ serde_json = { workspace = true }
 serde_with = { workspace = true }
 serde-xml-rs = { workspace = true }
 specific-models = { path = "../../models/specific" }
+specific-utils = { path = "../../utils/specific" }
 supporting-service = { path = "../supporting" }
 tracing = { workspace = true }
 traits = { path = "../../traits" }
diff --git a/crates/services/importer/src/anilist.rs b/crates/services/importer/src/anilist.rs
index 967c18d696..40d45756ce 100644
--- a/crates/services/importer/src/anilist.rs
+++ b/crates/services/importer/src/anilist.rs
@@ -1,10 +1,20 @@
-use std::{collections::HashMap, fs};
+use std::{collections::HashMap, fs, sync::Arc};
 
 use async_graphql::Result;
-use dependent_models::ImportResult;
-use media_models::DeployJsonImportInput;
+use chrono::NaiveDateTime;
+use dependent_models::{ImportCompletedItem, ImportResult};
+use enum_models::{ImportSource, MediaLot, MediaSource, Visibility};
+use media_models::{
+    DeployJsonImportInput, ImportOrExportItemRating, ImportOrExportItemReview,
+    ImportOrExportMetadataItem, ImportOrExportMetadataItemSeen,
+};
 use nest_struct::nest_struct;
+use rust_decimal::Decimal;
+use rust_decimal_macros::dec;
 use serde::Deserialize;
+use supporting_service::SupportingService;
+
+use super::utils;
 
 #[nest_struct]
 #[derive(Debug, Deserialize)]
@@ -17,45 +27,177 @@ struct AnilistExport {
     },
     lists: Vec<
         nest! {
-            score: u8,
+            id: u64,
             progress: u8,
+            score: Decimal,
             series_id: i32,
             series_type: u8,
-            started_on: u64,
-            finished_on: u64,
+            updated_at: String,
+            progress_volume: u8,
             custom_lists: String,
+            notes: Option<String>,
         },
     >,
     reviews: Vec<
         nest! {
-            score: u8,
+            id: u64,
             private: u8,
             text: String,
             series_id: i32,
+            score: Decimal,
             summary: String,
+            series_type: u8,
+            updated_at: String,
+        },
+    >,
+    favourites: Vec<
+        nest! {
+            favourite_id: i32,
+            favourite_type: u8,
         },
     >,
 }
 
-pub async fn import(input: DeployJsonImportInput) -> Result<ImportResult> {
+pub async fn import(
+    input: DeployJsonImportInput,
+    ss: &Arc<SupportingService>,
+) -> Result<ImportResult> {
     let export = fs::read_to_string(input.export)?;
     let data = serde_json::from_str::<AnilistExport>(&export)?;
-    let lists = data.user.custom_lists;
-    let anime_custom_lists = lists
+    let user_lists = data.user.custom_lists;
+    let mut completed = vec![];
+    let anime_custom_lists = user_lists
         .anime
         .into_iter()
         .enumerate()
-        .map(|(idx, list_name)| (idx + 1, list_name))
         .collect::<HashMap<_, _>>();
-    let manga_custom_lists = lists
+    let manga_custom_lists = user_lists
         .manga
         .into_iter()
         .enumerate()
-        .map(|(idx, list_name)| (idx + 1, list_name))
         .collect::<HashMap<_, _>>();
-    dbg!(anime_custom_lists, manga_custom_lists);
-    todo!();
+    for item in data.lists {
+        let progress = [item.progress, item.progress_volume].into_iter().max();
+        let lot = anilist_series_type_to_lot(item.series_type);
+        let mut to_push_item = ImportOrExportMetadataItem {
+            lot,
+            source: MediaSource::Anilist,
+            source_id: item.id.to_string(),
+            identifier: item.series_id.to_string(),
+            ..Default::default()
+        };
+        for num in 1..progress.unwrap_or_default() + 1 {
+            let mut history = ImportOrExportMetadataItemSeen {
+                provider_watched_on: Some(ImportSource::Anilist.to_string()),
+                ended_on: Some(parse_date_string(&item.updated_at).date()),
+                ..Default::default()
+            };
+            match lot {
+                MediaLot::Anime => {
+                    history.anime_episode_number = Some(num.into());
+                }
+                MediaLot::Manga => {
+                    history.manga_chapter_number = Some(num.into());
+                }
+                _ => unreachable!(),
+            }
+            to_push_item.seen_history.push(history);
+        }
+        let in_lists = serde_json::from_str::<Vec<usize>>(&item.custom_lists)?;
+        for in_list in in_lists.iter() {
+            match lot {
+                MediaLot::Anime => {
+                    if let Some(list) = anime_custom_lists.get(in_list) {
+                        to_push_item.collections.push(list.clone());
+                    }
+                }
+                MediaLot::Manga => {
+                    if let Some(list) = manga_custom_lists.get(in_list) {
+                        to_push_item.collections.push(list.clone());
+                    }
+                }
+                _ => unreachable!(),
+            }
+        }
+        let mut default_review = ImportOrExportItemRating {
+            ..Default::default()
+        };
+        if item.score > dec!(0) {
+            default_review.rating = Some(item.score);
+        }
+        if let Some(notes) = item.notes {
+            if !notes.is_empty() {
+                default_review.review = Some(ImportOrExportItemReview {
+                    text: Some(notes),
+                    ..Default::default()
+                });
+            }
+        }
+        to_push_item.reviews.push(default_review);
+        completed.push(ImportCompletedItem::Metadata(to_push_item));
+    }
+    for review in data.reviews {
+        let lot = anilist_series_type_to_lot(review.series_type);
+        let visibility = anilist_private_status_to_visibility(review.private);
+        let entire_text = format!("{}\n\n{}", review.summary, review.text);
+        completed.push(ImportCompletedItem::Metadata(ImportOrExportMetadataItem {
+            lot,
+            source: MediaSource::Anilist,
+            source_id: review.id.to_string(),
+            identifier: review.series_id.to_string(),
+            reviews: vec![ImportOrExportItemRating {
+                rating: Some(review.score),
+                review: Some(ImportOrExportItemReview {
+                    text: Some(entire_text),
+                    visibility: Some(visibility),
+                    date: Some(utils::get_date_time_with_offset(
+                        parse_date_string(&review.updated_at),
+                        &ss.timezone,
+                    )),
+                    ..Default::default()
+                }),
+                ..Default::default()
+            }],
+            ..Default::default()
+        }));
+    }
+    for favorite in data.favourites {
+        let lot = anilist_favorite_type_to_lot(favorite.favourite_type);
+        completed.push(ImportCompletedItem::Metadata(ImportOrExportMetadataItem {
+            lot,
+            source: MediaSource::Anilist,
+            collections: vec!["Favorite".to_string()],
+            identifier: favorite.favourite_id.to_string(),
+            ..Default::default()
+        }));
+    }
     Ok(ImportResult {
+        completed,
         ..Default::default()
     })
 }
+
+fn parse_date_string(input: &str) -> NaiveDateTime {
+    NaiveDateTime::parse_from_str(input, "%Y-%m-%d %H:%M:%S").unwrap()
+}
+
+fn anilist_series_type_to_lot(series_type: u8) -> MediaLot {
+    match series_type {
+        1 => MediaLot::Manga,
+        _ => MediaLot::Anime,
+    }
+}
+
+fn anilist_favorite_type_to_lot(favourite_type: u8) -> MediaLot {
+    match favourite_type {
+        1 => MediaLot::Anime,
+        _ => MediaLot::Manga,
+    }
+}
+
+fn anilist_private_status_to_visibility(private: u8) -> Visibility {
+    match private {
+        1 => Visibility::Private,
+        _ => Visibility::Public,
+    }
+}
diff --git a/crates/services/importer/src/audiobookshelf.rs b/crates/services/importer/src/audiobookshelf.rs
index ce1b577638..4226f776e2 100644
--- a/crates/services/importer/src/audiobookshelf.rs
+++ b/crates/services/importer/src/audiobookshelf.rs
@@ -1,16 +1,17 @@
-use std::future::Future;
+use std::{future::Future, sync::Arc};
 
 use anyhow::anyhow;
 use application_utils::{get_base_http_client, get_podcast_episode_number_by_name};
 use async_graphql::Result;
+use common_models::StringIdObject;
 use common_utils::ryot_log;
 use data_encoding::BASE64;
-use database_models::metadata;
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::{ImportSource, MediaLot, MediaSource};
+use dependent_utils::get_identifier_from_book_isbn;
+use enum_models::{ImportSource, MediaLot, MediaSource};
 use media_models::{
-    CommitMediaInput, DeployUrlAndKeyImportInput, ImportOrExportMetadataItem,
-    ImportOrExportMetadataItemSeen,
+    DeployUrlAndKeyImportInput, ImportOrExportMetadataItem, ImportOrExportMetadataItemSeen,
+    UniqueMediaIdentifier,
 };
 use providers::{google_books::GoogleBooksService, openlibrary::OpenlibraryService};
 use reqwest::{
@@ -19,17 +20,20 @@ use reqwest::{
 };
 use serde_json::json;
 use specific_models::audiobookshelf as audiobookshelf_models;
+use specific_utils::audiobookshelf::get_updated_metadata;
+use supporting_service::SupportingService;
 
-use super::{utils, ImportFailStep, ImportFailedItem};
+use super::{ImportFailStep, ImportFailedItem};
 
 pub async fn import<F>(
     input: DeployUrlAndKeyImportInput,
+    ss: &Arc<SupportingService>,
     google_books_service: &GoogleBooksService,
     open_library_service: &OpenlibraryService,
-    commit_metadata: impl Fn(CommitMediaInput) -> F,
+    commit_metadata: impl Fn(UniqueMediaIdentifier) -> F,
 ) -> Result<ImportResult>
 where
-    F: Future<Output = Result<metadata::Model>>,
+    F: Future<Output = Result<StringIdObject>>,
 {
     let mut completed = vec![];
     let mut failed = vec![];
@@ -70,7 +74,7 @@ where
                 == item.media.as_ref().unwrap().ebook_format
             {
                 match &metadata.isbn {
-                    Some(isbn) => match utils::get_identifier_from_book_isbn(
+                    Some(isbn) => match get_identifier_from_book_isbn(
                         isbn,
                         google_books_service,
                         open_library_service,
@@ -119,13 +123,13 @@ where
                             if let Some(true) =
                                 episode_details.user_media_progress.map(|u| u.is_finished)
                             {
-                                let podcast = commit_metadata(CommitMediaInput {
-                                    identifier: itunes_id.clone(),
+                                commit_metadata(UniqueMediaIdentifier {
                                     lot,
                                     source,
-                                    ..Default::default()
+                                    identifier: itunes_id.clone(),
                                 })
                                 .await?;
+                                let podcast = get_updated_metadata(&itunes_id, ss).await?;
                                 if let Some(pe) = podcast.podcast_specifics.and_then(|p| {
                                     get_podcast_episode_number_by_name(&p, &episode.title)
                                 }) {
diff --git a/crates/services/importer/src/generic_json.rs b/crates/services/importer/src/generic_json.rs
index 80967dccef..75e6e97a0b 100644
--- a/crates/services/importer/src/generic_json.rs
+++ b/crates/services/importer/src/generic_json.rs
@@ -2,7 +2,7 @@ use std::fs;
 
 use async_graphql::Result;
 use dependent_models::{CompleteExport, ImportCompletedItem, ImportResult};
-use enums::ImportSource;
+use enum_models::ImportSource;
 use itertools::Itertools;
 use media_models::DeployJsonImportInput;
 
diff --git a/crates/services/importer/src/goodreads.rs b/crates/services/importer/src/goodreads.rs
index d2304e4340..6f4b342a72 100644
--- a/crates/services/importer/src/goodreads.rs
+++ b/crates/services/importer/src/goodreads.rs
@@ -4,7 +4,8 @@ use common_utils::ryot_log;
 use convert_case::{Case, Casing};
 use csv::Reader;
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::{ImportSource, MediaLot};
+use dependent_utils::get_identifier_from_book_isbn;
+use enum_models::{ImportSource, MediaLot};
 use itertools::Itertools;
 use media_models::{
     DeployGenericCsvImportInput, ImportOrExportItemRating, ImportOrExportItemReview,
@@ -15,7 +16,7 @@ use rust_decimal::Decimal;
 use rust_decimal_macros::dec;
 use serde::Deserialize;
 
-use super::{utils, ImportFailStep, ImportFailedItem};
+use super::{ImportFailStep, ImportFailedItem};
 
 #[derive(Debug, Deserialize)]
 struct Book {
@@ -77,8 +78,7 @@ pub async fn import(
             continue;
         }
         let Some((identifier, source)) =
-            utils::get_identifier_from_book_isbn(&isbn, google_books_service, open_library_service)
-                .await
+            get_identifier_from_book_isbn(&isbn, google_books_service, open_library_service).await
         else {
             failed.push(ImportFailedItem {
                 lot: Some(lot),
diff --git a/crates/services/importer/src/hevy.rs b/crates/services/importer/src/hevy.rs
index 706274f9f7..eb6d2ffc60 100644
--- a/crates/services/importer/src/hevy.rs
+++ b/crates/services/importer/src/hevy.rs
@@ -6,7 +6,7 @@ use common_utils::ryot_log;
 use csv::Reader;
 use database_models::exercise;
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::ExerciseLot;
+use enum_models::ExerciseLot;
 use fitness_models::{
     SetLot, UserExerciseInput, UserWorkoutInput, UserWorkoutSetRecord, WorkoutSetStatistic,
 };
diff --git a/crates/services/importer/src/igdb.rs b/crates/services/importer/src/igdb.rs
index ace062a5f8..3afa1b2d7c 100644
--- a/crates/services/importer/src/igdb.rs
+++ b/crates/services/importer/src/igdb.rs
@@ -2,7 +2,7 @@ use async_graphql::Result;
 use common_models::DefaultCollection;
 use csv::Reader;
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use itertools::Itertools;
 use media_models::{
     DeployIgdbImportInput, ImportOrExportMetadataItem, ImportOrExportMetadataItemSeen,
diff --git a/crates/services/importer/src/imdb.rs b/crates/services/importer/src/imdb.rs
index cd5bf7e7d1..58beb46ef7 100644
--- a/crates/services/importer/src/imdb.rs
+++ b/crates/services/importer/src/imdb.rs
@@ -3,7 +3,7 @@ use common_models::DefaultCollection;
 use common_utils::ryot_log;
 use csv::Reader;
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use itertools::Itertools;
 use media_models::{DeployGenericCsvImportInput, ImportOrExportMetadataItem};
 pub use providers::tmdb::NonMediaTmdbService;
diff --git a/crates/services/importer/src/jellyfin.rs b/crates/services/importer/src/jellyfin.rs
index 751256f205..a86a16820b 100644
--- a/crates/services/importer/src/jellyfin.rs
+++ b/crates/services/importer/src/jellyfin.rs
@@ -2,7 +2,7 @@ use async_graphql::Result;
 use common_utils::ryot_log;
 use dependent_models::{ImportCompletedItem, ImportResult};
 use enum_meta::HashMap;
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use external_utils::jellyfin::{get_authenticated_client, ItemResponse, ItemsResponse, MediaType};
 use media_models::{
     DeployUrlAndKeyAndUsernameImportInput, ImportOrExportMetadataItem,
diff --git a/crates/services/importer/src/lib.rs b/crates/services/importer/src/lib.rs
index 5379d6ae27..0a0361a4e7 100644
--- a/crates/services/importer/src/lib.rs
+++ b/crates/services/importer/src/lib.rs
@@ -1,7 +1,7 @@
 use std::{collections::HashMap, sync::Arc};
 
 use async_graphql::Result;
-use background::ApplicationJob;
+use background_models::{ApplicationJob, MpApplicationJob};
 use chrono::{DateTime, Duration, NaiveDateTime, Offset, TimeZone, Utc};
 use common_models::BackgroundJob;
 use common_utils::ryot_log;
@@ -13,11 +13,10 @@ use dependent_utils::{
     commit_metadata, deploy_background_job, generate_exercise_id, get_google_books_service,
     get_openlibrary_service, get_tmdb_non_media_service, process_import,
 };
-use enums::{ExerciseLot, ExerciseSource};
-use enums::{ImportSource, MediaSource};
+use enum_models::ImportSource;
+use enum_models::{ExerciseLot, ExerciseSource};
 use importer_models::{ImportFailStep, ImportFailedItem};
-use media_models::{DeployImportJobInput, ImportOrExportMetadataItem};
-use providers::{google_books::GoogleBooksService, openlibrary::OpenlibraryService};
+use media_models::{CommitMediaInput, DeployImportJobInput, ImportOrExportMetadataItem};
 use rust_decimal_macros::dec;
 use sea_orm::{
     prelude::Expr, ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, QueryFilter, QueryOrder,
@@ -50,8 +49,10 @@ impl ImporterService {
         user_id: String,
         input: DeployImportJobInput,
     ) -> Result<bool> {
-        let job = ApplicationJob::ImportFromExternalSource(user_id, Box::new(input));
-        self.0.perform_application_job(job).await?;
+        let job = MpApplicationJob::ImportFromExternalSource(user_id, Box::new(input));
+        self.0
+            .perform_application_job(ApplicationJob::Mp(job))
+            .await?;
         ryot_log!(debug, "Deployed import job");
         Ok(true)
     }
@@ -66,7 +67,7 @@ impl ImporterService {
         Ok(reports)
     }
 
-    pub async fn start_importing(
+    pub async fn perform_import(
         &self,
         user_id: String,
         input: Box<DeployImportJobInput>,
@@ -81,7 +82,7 @@ impl ImporterService {
         let import_id = db_import_job.id.clone();
         ryot_log!(debug, "Started import job with id {import_id}");
         let maybe_import = match input.source {
-            ImportSource::Anilist => anilist::import(input.generic_json.unwrap()).await,
+            ImportSource::Anilist => anilist::import(input.generic_json.unwrap(), &self.0).await,
             ImportSource::StrongApp => {
                 strong_app::import(input.strong_app.unwrap(), &self.0, &user_id).await
             }
@@ -109,9 +110,18 @@ impl ImporterService {
             ImportSource::Audiobookshelf => {
                 audiobookshelf::import(
                     input.url_and_key.unwrap(),
+                    &self.0,
                     &get_google_books_service(&self.0.config).await.unwrap(),
                     &get_openlibrary_service(&self.0.config).await.unwrap(),
-                    |input| commit_metadata(input, &self.0),
+                    |input| {
+                        commit_metadata(
+                            CommitMediaInput {
+                                unique: input,
+                                name: "Loading...".to_owned(),
+                            },
+                            &self.0,
+                        )
+                    },
                 )
                 .await
             }
@@ -133,7 +143,7 @@ impl ImporterService {
         let mut model: import_report::ActiveModel = db_import_job.into();
         match maybe_import {
             Ok(import) => {
-                match process_import(&user_id, false, import, &self.0, |progress| {
+                match process_import(&user_id, true, false, import, &self.0, |progress| {
                     let id = import_id.clone();
                     async move {
                         ImportReport::update_many()
@@ -191,22 +201,6 @@ pub mod utils {
         DateTime::<Utc>::from_naive_utc_and_offset(date_time, Utc) - offset
     }
 
-    pub async fn get_identifier_from_book_isbn(
-        isbn: &str,
-        google_books_service: &GoogleBooksService,
-        open_library_service: &OpenlibraryService,
-    ) -> Option<(String, MediaSource)> {
-        let mut identifier = None;
-        let mut source = MediaSource::GoogleBooks;
-        if let Some(id) = google_books_service.id_from_isbn(isbn).await {
-            identifier = Some(id);
-        } else if let Some(id) = open_library_service.id_from_isbn(isbn).await {
-            identifier = Some(id);
-            source = MediaSource::Openlibrary;
-        }
-        identifier.map(|id| (id, source))
-    }
-
     pub async fn associate_with_existing_or_new_exercise(
         user_id: &str,
         exercise_name: &String,
diff --git a/crates/services/importer/src/mediatracker.rs b/crates/services/importer/src/mediatracker.rs
index 1ab4684f43..78c1ea87a3 100644
--- a/crates/services/importer/src/mediatracker.rs
+++ b/crates/services/importer/src/mediatracker.rs
@@ -2,7 +2,7 @@ use async_graphql::Result;
 use common_models::IdObject;
 use common_utils::{ryot_log, USER_AGENT_STR};
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::{ImportSource, MediaLot, MediaSource};
+use enum_models::{ImportSource, MediaLot, MediaSource};
 use media_models::{
     CreateOrUpdateCollectionInput, DeployUrlAndKeyImportInput, ImportOrExportItemRating,
     ImportOrExportItemReview, ImportOrExportMetadataItemSeen,
diff --git a/crates/services/importer/src/movary.rs b/crates/services/importer/src/movary.rs
index 08b2a90305..fb147567a7 100644
--- a/crates/services/importer/src/movary.rs
+++ b/crates/services/importer/src/movary.rs
@@ -4,7 +4,7 @@ use common_models::DefaultCollection;
 use common_utils::convert_naive_to_utc;
 use csv::Reader;
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::{ImportSource, MediaLot, MediaSource};
+use enum_models::{ImportSource, MediaLot, MediaSource};
 use media_models::{
     DeployMovaryImportInput, ImportOrExportItemRating, ImportOrExportItemReview,
     ImportOrExportMetadataItemSeen,
diff --git a/crates/services/importer/src/myanimelist.rs b/crates/services/importer/src/myanimelist.rs
index 6db8b11573..d05fe8fdaf 100644
--- a/crates/services/importer/src/myanimelist.rs
+++ b/crates/services/importer/src/myanimelist.rs
@@ -7,7 +7,7 @@ use async_graphql::Result;
 use chrono::NaiveDate;
 use common_utils::convert_string_to_date;
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::{ImportSource, MediaLot, MediaSource};
+use enum_models::{ImportSource, MediaLot, MediaSource};
 use flate2::bufread::GzDecoder;
 use itertools::Itertools;
 use media_models::{
diff --git a/crates/services/importer/src/plex.rs b/crates/services/importer/src/plex.rs
index 5c32047d61..b7c1c36b44 100644
--- a/crates/services/importer/src/plex.rs
+++ b/crates/services/importer/src/plex.rs
@@ -2,7 +2,7 @@ use application_utils::get_base_http_client;
 use async_graphql::Result;
 use common_utils::ryot_log;
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::{ImportSource, MediaLot, MediaSource};
+use enum_models::{ImportSource, MediaLot, MediaSource};
 use importer_models::{ImportFailStep, ImportFailedItem};
 use media_models::{
     DeployUrlAndKeyImportInput, ImportOrExportMetadataItem, ImportOrExportMetadataItemSeen,
@@ -63,8 +63,8 @@ pub async fn import(input: DeployUrlAndKeyImportInput) -> Result<ImportResult> {
             else {
                 failed_items.push(ImportFailedItem {
                     lot: Some(lot),
-                    identifier: item.key.clone(),
                     step: ImportFailStep::ItemDetailsFromSource,
+                    identifier: format!("{} ({}) - {}", item.title, lot, item.key),
                     error: Some("No TMDb ID associated with this media".to_string()),
                 });
                 continue;
@@ -116,9 +116,7 @@ pub async fn import(input: DeployUrlAndKeyImportInput) -> Result<ImportResult> {
                             });
                         }
                     }
-                    if !item.seen_history.is_empty() {
-                        success_items.push(ImportCompletedItem::Metadata(item));
-                    }
+                    success_items.push(ImportCompletedItem::Metadata(item));
                 }
                 _ => unreachable!(),
             }
diff --git a/crates/services/importer/src/storygraph.rs b/crates/services/importer/src/storygraph.rs
index 457470937e..7474f51153 100644
--- a/crates/services/importer/src/storygraph.rs
+++ b/crates/services/importer/src/storygraph.rs
@@ -4,7 +4,8 @@ use common_utils::ryot_log;
 use convert_case::{Case, Casing};
 use csv::Reader;
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::{ImportSource, MediaLot};
+use dependent_utils::get_identifier_from_book_isbn;
+use enum_models::{ImportSource, MediaLot};
 use itertools::Itertools;
 use media_models::{
     DeployGenericCsvImportInput, ImportOrExportItemRating, ImportOrExportItemReview,
@@ -15,7 +16,7 @@ use rust_decimal::Decimal;
 use rust_decimal_macros::dec;
 use serde::{Deserialize, Serialize};
 
-use super::{utils, ImportFailStep, ImportFailedItem, ImportOrExportMetadataItem};
+use super::{ImportFailStep, ImportFailedItem, ImportOrExportMetadataItem};
 
 #[derive(Debug, Serialize, Deserialize)]
 #[serde(untagged)]
@@ -88,8 +89,7 @@ pub async fn import(
             continue;
         };
         let Some((identifier, source)) =
-            utils::get_identifier_from_book_isbn(&isbn, google_books_service, open_library_service)
-                .await
+            get_identifier_from_book_isbn(&isbn, google_books_service, open_library_service).await
         else {
             failed.push(ImportFailedItem {
                 lot: Some(lot),
diff --git a/crates/services/importer/src/strong_app.rs b/crates/services/importer/src/strong_app.rs
index 58e236c8a8..4da294153c 100644
--- a/crates/services/importer/src/strong_app.rs
+++ b/crates/services/importer/src/strong_app.rs
@@ -6,7 +6,7 @@ use common_utils::ryot_log;
 use csv::ReaderBuilder;
 use database_models::exercise;
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::ExerciseLot;
+use enum_models::ExerciseLot;
 use fitness_models::{
     SetLot, UserExerciseInput, UserWorkoutInput, UserWorkoutSetRecord, WorkoutSetStatistic,
 };
diff --git a/crates/services/importer/src/trakt.rs b/crates/services/importer/src/trakt.rs
index 5cb3ce4dc7..1389159a9d 100644
--- a/crates/services/importer/src/trakt.rs
+++ b/crates/services/importer/src/trakt.rs
@@ -3,7 +3,7 @@ use async_graphql::Result;
 use common_utils::{ryot_log, APPLICATION_JSON_HEADER};
 use convert_case::{Case, Casing};
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::{ImportSource, MediaLot, MediaSource};
+use enum_models::{ImportSource, MediaLot, MediaSource};
 use env_utils::TRAKT_CLIENT_ID;
 use itertools::Itertools;
 use media_models::{
@@ -223,12 +223,9 @@ pub async fn import(input: DeployTraktImportInput) -> Result<ImportResult> {
     completed.extend(lists.iter().map(|l| {
         ImportCompletedItem::Collection(CreateOrUpdateCollectionInput {
             name: l.name.to_case(Case::Title),
-            description: l.description.as_ref().and_then(|s| {
-                if s.is_empty() {
-                    None
-                } else {
-                    Some(s.to_owned())
-                }
+            description: l.description.as_ref().and_then(|s| match s.is_empty() {
+                true => None,
+                false => Some(s.to_owned()),
             }),
             ..Default::default()
         })
diff --git a/crates/services/integration/Cargo.toml b/crates/services/integration/Cargo.toml
index 996a58f121..af7dcdf480 100644
--- a/crates/services/integration/Cargo.toml
+++ b/crates/services/integration/Cargo.toml
@@ -14,7 +14,7 @@ database-models = { path = "../../models/database" }
 database-utils = { path = "../../utils/database" }
 dependent-models = { path = "../../models/dependent" }
 dependent-utils = { path = "../../utils/dependent" }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 eventsource-stream = { workspace = true }
 external-utils = { path = "../../utils/external" }
 itertools = { workspace = true }
@@ -31,6 +31,7 @@ serde = { workspace = true }
 serde_json = { workspace = true }
 sonarr-api-rs = { workspace = true }
 specific-models = { path = "../../models/specific" }
+specific-utils = { path = "../../utils/specific" }
 supporting-service = { path = "../supporting" }
 tokio = { workspace = true }
 tracing = { workspace = true }
diff --git a/crates/services/integration/src/lib.rs b/crates/services/integration/src/lib.rs
index a19380d858..968d3034e1 100644
--- a/crates/services/integration/src/lib.rs
+++ b/crates/services/integration/src/lib.rs
@@ -11,9 +11,9 @@ use database_models::{
 use database_utils::user_by_id;
 use dependent_models::{ImportCompletedItem, ImportResult};
 use dependent_utils::{commit_metadata, process_import};
-use enums::{EntityLot, IntegrationLot, IntegrationProvider, MediaLot};
-use media_models::SeenShowExtraInformation;
-use providers::google_books::GoogleBooksService;
+use enum_models::{EntityLot, IntegrationLot, IntegrationProvider, MediaLot};
+use media_models::{CommitMediaInput, SeenShowExtraInformation};
+use providers::{google_books::GoogleBooksService, openlibrary::OpenlibraryService};
 use rust_decimal_macros::dec;
 use sea_orm::{ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, QueryFilter, QuerySelect};
 use supporting_service::SupportingService;
@@ -65,9 +65,14 @@ impl IntegrationService {
                 });
             }
         });
-        match process_import(&integration.user_id, true, import, &self.0, |_| async {
-            Ok(())
-        })
+        match process_import(
+            &integration.user_id,
+            false,
+            true,
+            import,
+            &self.0,
+            |_| async { Ok(()) },
+        )
         .await
         {
             Ok(_) => {
@@ -241,10 +246,10 @@ impl IntegrationService {
         Ok(())
     }
 
-    pub async fn yank_integrations_data_for_user(&self, user_id: &String) -> GqlResult<bool> {
+    pub async fn yank_integrations_data_for_user(&self, user_id: &String) -> GqlResult<()> {
         let preferences = user_by_id(user_id, &self.0).await?.preferences;
         if preferences.general.disable_integrations {
-            return Ok(false);
+            return Ok(());
         }
         let integrations = Integration::find()
             .filter(integration::Column::UserId.eq(user_id))
@@ -263,8 +268,18 @@ impl IntegrationService {
                     yank::audiobookshelf::yank_progress(
                         specifics.audiobookshelf_base_url.unwrap(),
                         specifics.audiobookshelf_token.unwrap(),
-                        GoogleBooksService::new(&self.0.config.books.google_books).await,
-                        |input| commit_metadata(input, &self.0),
+                        &self.0,
+                        &GoogleBooksService::new(&self.0.config.books.google_books).await,
+                        &OpenlibraryService::new(&self.0.config.books.openlibrary).await,
+                        |input| {
+                            commit_metadata(
+                                CommitMediaInput {
+                                    unique: input,
+                                    name: "Loading...".to_owned(),
+                                },
+                                &self.0,
+                            )
+                        },
                     )
                     .await
                 }
@@ -289,7 +304,7 @@ impl IntegrationService {
                 .await
                 .trace_ok();
         }
-        Ok(true)
+        Ok(())
     }
 
     pub async fn yank_integrations_data(&self) -> GqlResult<()> {
diff --git a/crates/services/integration/src/push/jellyfin.rs b/crates/services/integration/src/push/jellyfin.rs
index f7b73031d5..a8f0945ec3 100644
--- a/crates/services/integration/src/push/jellyfin.rs
+++ b/crates/services/integration/src/push/jellyfin.rs
@@ -1,6 +1,6 @@
 use anyhow::Result;
 use common_utils::ryot_log;
-use enums::MediaLot;
+use enum_models::MediaLot;
 use external_utils::jellyfin::{get_authenticated_client, ItemsResponse};
 use media_models::SeenShowExtraInformation;
 use serde_json::json;
diff --git a/crates/services/integration/src/push/radarr.rs b/crates/services/integration/src/push/radarr.rs
index 57960f32a9..29e58d5251 100644
--- a/crates/services/integration/src/push/radarr.rs
+++ b/crates/services/integration/src/push/radarr.rs
@@ -1,6 +1,6 @@
 use anyhow::Result;
 use common_utils::ryot_log;
-use enums::MediaLot;
+use enum_models::MediaLot;
 use radarr_api_rs::{
     apis::{
         configuration::{ApiKey as RadarrApiKey, Configuration as RadarrConfiguration},
diff --git a/crates/services/integration/src/push/sonarr.rs b/crates/services/integration/src/push/sonarr.rs
index e77a29a6f2..9a1f561b13 100644
--- a/crates/services/integration/src/push/sonarr.rs
+++ b/crates/services/integration/src/push/sonarr.rs
@@ -1,6 +1,6 @@
 use anyhow::Result;
 use common_utils::ryot_log;
-use enums::MediaLot;
+use enum_models::MediaLot;
 use sonarr_api_rs::{
     apis::{
         configuration::{ApiKey as SonarrApiKey, Configuration as SonarrConfiguration},
diff --git a/crates/services/integration/src/sink/emby.rs b/crates/services/integration/src/sink/emby.rs
index f5da9fe6c0..db849b14a2 100644
--- a/crates/services/integration/src/sink/emby.rs
+++ b/crates/services/integration/src/sink/emby.rs
@@ -1,6 +1,6 @@
 use anyhow::{bail, Result};
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use media_models::{ImportOrExportMetadataItem, ImportOrExportMetadataItemSeen};
 use rust_decimal::Decimal;
 use rust_decimal_macros::dec;
diff --git a/crates/services/integration/src/sink/jellyfin.rs b/crates/services/integration/src/sink/jellyfin.rs
index 182fbb5a75..b5e174b250 100644
--- a/crates/services/integration/src/sink/jellyfin.rs
+++ b/crates/services/integration/src/sink/jellyfin.rs
@@ -1,6 +1,6 @@
 use anyhow::{bail, Result};
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use media_models::{ImportOrExportMetadataItem, ImportOrExportMetadataItemSeen};
 use rust_decimal::Decimal;
 use rust_decimal_macros::dec;
diff --git a/crates/services/integration/src/sink/kodi.rs b/crates/services/integration/src/sink/kodi.rs
index 1bbe987b61..55326a8d08 100644
--- a/crates/services/integration/src/sink/kodi.rs
+++ b/crates/services/integration/src/sink/kodi.rs
@@ -1,6 +1,6 @@
 use anyhow::{bail, Result};
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use media_models::{ImportOrExportMetadataItem, ImportOrExportMetadataItemSeen};
 use rust_decimal::Decimal;
 use serde::{Deserialize, Serialize};
diff --git a/crates/services/integration/src/sink/plex.rs b/crates/services/integration/src/sink/plex.rs
index 6b125de088..1f8ce65976 100644
--- a/crates/services/integration/src/sink/plex.rs
+++ b/crates/services/integration/src/sink/plex.rs
@@ -1,7 +1,7 @@
 use anyhow::{bail, Context, Result};
 use common_models::StringIdObject;
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use media_models::{ImportOrExportMetadataItem, ImportOrExportMetadataItemSeen};
 use regex::Regex;
 use rust_decimal::Decimal;
diff --git a/crates/services/integration/src/utils.rs b/crates/services/integration/src/utils.rs
index d11bb786cb..7e3d89e759 100644
--- a/crates/services/integration/src/utils.rs
+++ b/crates/services/integration/src/utils.rs
@@ -1,7 +1,7 @@
 use anyhow::{bail, Result};
 use database_models::metadata;
 use database_utils::ilike_sql;
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use sea_orm::{ColumnTrait, Condition, DatabaseConnection, EntityTrait, QueryFilter};
 use sea_query::{extension::postgres::PgExpr, Alias, Expr, Func};
 
diff --git a/crates/services/integration/src/yank/audiobookshelf.rs b/crates/services/integration/src/yank/audiobookshelf.rs
index 8d6979d838..203e013d87 100644
--- a/crates/services/integration/src/yank/audiobookshelf.rs
+++ b/crates/services/integration/src/yank/audiobookshelf.rs
@@ -1,21 +1,25 @@
-use std::future::Future;
+use std::{future::Future, sync::Arc};
 
 use anyhow::{anyhow, Result};
 use application_utils::{get_base_http_client, get_podcast_episode_number_by_name};
 use async_graphql::Result as GqlResult;
-use common_models::DefaultCollection;
+use common_models::{DefaultCollection, StringIdObject};
 use common_utils::ryot_log;
-use database_models::metadata;
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::{MediaLot, MediaSource};
-use media_models::{CommitMediaInput, ImportOrExportMetadataItem, ImportOrExportMetadataItemSeen};
-use providers::google_books::GoogleBooksService;
+use dependent_utils::get_identifier_from_book_isbn;
+use enum_models::{MediaLot, MediaSource};
+use media_models::{
+    ImportOrExportMetadataItem, ImportOrExportMetadataItemSeen, UniqueMediaIdentifier,
+};
+use providers::{google_books::GoogleBooksService, openlibrary::OpenlibraryService};
 use reqwest::{
     header::{HeaderValue, AUTHORIZATION},
     Client,
 };
 use rust_decimal_macros::dec;
 use specific_models::audiobookshelf::{self, LibrariesListResponse, ListResponse};
+use specific_utils::audiobookshelf::get_updated_metadata;
+use supporting_service::SupportingService;
 
 fn get_http_client(access_token: &String) -> Client {
     get_base_http_client(Some(vec![(
@@ -27,12 +31,13 @@ fn get_http_client(access_token: &String) -> Client {
 pub async fn yank_progress<F>(
     base_url: String,
     access_token: String,
-    // TODO: Find a way to use `get_identifier_from_book_isbn` function
-    isbn_service: GoogleBooksService,
-    commit_metadata: impl Fn(CommitMediaInput) -> F,
+    ss: &Arc<SupportingService>,
+    google_books_service: &GoogleBooksService,
+    open_library_service: &OpenlibraryService,
+    commit_metadata: impl Fn(UniqueMediaIdentifier) -> F,
 ) -> Result<ImportResult>
 where
-    F: Future<Output = GqlResult<metadata::Model>>,
+    F: Future<Output = GqlResult<StringIdObject>>,
 {
     let url = format!("{}/api", base_url);
     let client = get_http_client(&access_token);
@@ -55,14 +60,14 @@ where
         let (progress_id, identifier, lot, source, podcast_episode_number) =
             if Some("epub".to_string()) == item.media.as_ref().unwrap().ebook_format {
                 match &metadata.isbn {
-                    Some(isbn) => match isbn_service.id_from_isbn(isbn).await {
-                        Some(id) => (
-                            item.id.clone(),
-                            id,
-                            MediaLot::Book,
-                            MediaSource::GoogleBooks,
-                            None,
-                        ),
+                    Some(isbn) => match get_identifier_from_book_isbn(
+                        isbn,
+                        google_books_service,
+                        open_library_service,
+                    )
+                    .await
+                    {
+                        Some(id) => (item.id.clone(), id.0, MediaLot::Book, id.1, None),
                         _ => {
                             ryot_log!(debug, "No Google Books ID found for ISBN {:#?}", isbn);
                             continue;
@@ -86,14 +91,14 @@ where
                     Some(pe) => {
                         let lot = MediaLot::Podcast;
                         let source = MediaSource::Itunes;
-                        let podcast = commit_metadata(CommitMediaInput {
-                            identifier: itunes_id.clone(),
+                        commit_metadata(UniqueMediaIdentifier {
                             lot,
                             source,
-                            ..Default::default()
+                            identifier: itunes_id.clone(),
                         })
                         .await
                         .unwrap();
+                        let podcast = get_updated_metadata(&itunes_id, ss).await?;
                         match podcast
                             .podcast_specifics
                             .and_then(|p| get_podcast_episode_number_by_name(&p, &pe.title))
diff --git a/crates/services/integration/src/yank/komga.rs b/crates/services/integration/src/yank/komga.rs
index fbd9c53c07..464b4ca080 100644
--- a/crates/services/integration/src/yank/komga.rs
+++ b/crates/services/integration/src/yank/komga.rs
@@ -10,10 +10,12 @@ use common_models::DefaultCollection;
 use common_utils::ryot_log;
 use database_models::{metadata, prelude::Metadata};
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use eventsource_stream::Eventsource;
 use itertools::Itertools;
-use media_models::{CommitMediaInput, ImportOrExportMetadataItem, ImportOrExportMetadataItemSeen};
+use media_models::{
+    ImportOrExportMetadataItem, ImportOrExportMetadataItemSeen, UniqueMediaIdentifier,
+};
 use reqwest::Url;
 use rust_decimal::{prelude::FromPrimitive, Decimal};
 use rust_decimal_macros::dec;
@@ -180,7 +182,7 @@ impl KomgaEventHandler {
     }
 }
 
-type ProcessEventReturn = (CommitMediaInput, ImportOrExportMetadataItemSeen);
+type ProcessEventReturn = (UniqueMediaIdentifier, ImportOrExportMetadataItemSeen);
 
 /// Generates the sse listener for komga. This is intended to be run from another
 /// thread if you run this in the main thread it will lock it up
@@ -357,11 +359,10 @@ async fn process_events(
     };
 
     Ok((
-        CommitMediaInput {
+        UniqueMediaIdentifier {
             source,
             identifier: id,
             lot: MediaLot::Manga,
-            force_update: None,
         },
         ImportOrExportMetadataItemSeen {
             progress: Some(calculate_percentage(
diff --git a/crates/services/integration/src/yank/plex.rs b/crates/services/integration/src/yank/plex.rs
index 83212a00a2..de995fee9e 100644
--- a/crates/services/integration/src/yank/plex.rs
+++ b/crates/services/integration/src/yank/plex.rs
@@ -3,7 +3,7 @@ use application_utils::get_base_http_client;
 use common_models::DefaultCollection;
 use common_utils::ryot_log;
 use dependent_models::{ImportCompletedItem, ImportResult};
-use enums::{MediaLot, MediaSource};
+use enum_models::{MediaLot, MediaSource};
 use media_models::ImportOrExportMetadataItem;
 use reqwest::header::{HeaderName, HeaderValue, ACCEPT};
 use specific_models::plex as plex_models;
diff --git a/crates/services/miscellaneous/Cargo.toml b/crates/services/miscellaneous/Cargo.toml
index c6b39224cd..33da72f4e0 100644
--- a/crates/services/miscellaneous/Cargo.toml
+++ b/crates/services/miscellaneous/Cargo.toml
@@ -6,7 +6,7 @@ edition = "2021"
 [dependencies]
 application-utils = { path = "../../utils/application" }
 async-graphql = { workspace = true }
-background = { path = "../../background" }
+background-models = { path = "../../models/background" }
 chrono = { workspace = true }
 common-models = { path = "../../models/common" }
 common-utils = { path = "../../utils/common" }
@@ -14,7 +14,7 @@ database-models = { path = "../../models/database" }
 database-utils = { path = "../../utils/database" }
 dependent-models = { path = "../../models/dependent" }
 dependent-utils = { path = "../../utils/dependent" }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 futures = { workspace = true }
 itertools = { workspace = true }
 markdown = { workspace = true }
diff --git a/crates/services/miscellaneous/src/lib.rs b/crates/services/miscellaneous/src/lib.rs
index 352e0e30cb..c46a55dfc7 100644
--- a/crates/services/miscellaneous/src/lib.rs
+++ b/crates/services/miscellaneous/src/lib.rs
@@ -9,7 +9,7 @@ use application_utils::{
     graphql_to_db_order,
 };
 use async_graphql::{Error, Result};
-use background::{ApplicationJob, CoreApplicationJob};
+use background_models::{ApplicationJob, HpApplicationJob, MpApplicationJob};
 use chrono::{Days, Duration, NaiveDate, Utc};
 use common_models::{
     ApplicationCacheKey, BackgroundJob, ChangeCollectionToEntityInput, DefaultCollection,
@@ -37,7 +37,7 @@ use database_models::{
 use database_utils::{
     apply_collection_filter, calculate_user_activities_and_summary, entity_in_collections,
     entity_in_collections_with_collection_to_entity_ids, ilike_sql, item_reviews,
-    remove_entity_from_collection, revoke_access_link, user_by_id,
+    revoke_access_link, user_by_id,
 };
 use dependent_models::{
     ApplicationCacheValue, CoreDetails, GenreDetails, MetadataBaseData, MetadataGroupDetails,
@@ -45,17 +45,17 @@ use dependent_models::{
     SearchResults, UserMetadataDetails, UserMetadataGroupDetails, UserPersonDetails,
 };
 use dependent_utils::{
-    add_entity_to_collection, commit_metadata, commit_metadata_group_internal,
-    commit_metadata_internal, commit_person, create_partial_metadata, create_user_notification,
+    add_entity_to_collection, change_metadata_associations, commit_metadata, commit_metadata_group,
+    commit_person, create_partial_metadata, create_user_notification,
     deploy_after_handle_media_seen_tasks, deploy_background_job, deploy_update_metadata_job,
     first_metadata_image_as_url, get_entity_recently_consumed, get_metadata_provider,
     get_openlibrary_service, get_tmdb_non_media_service, get_users_and_cte_monitoring_entity,
     get_users_monitoring_entity, handle_after_media_seen_tasks, is_metadata_finished_by_user,
     metadata_images_as_urls, post_review, progress_update,
     queue_media_state_changed_notification_for_user, refresh_collection_to_entity_association,
-    update_metadata_and_notify_users,
+    remove_entity_from_collection, update_metadata_and_notify_users,
 };
-use enums::{
+use enum_models::{
     EntityLot, MediaLot, MediaSource, MetadataToMetadataRelation, SeenState, UserNotificationLot,
     UserToMediaReason,
 };
@@ -68,15 +68,14 @@ use media_models::{
     GraphqlMediaAssets, GraphqlMetadataDetails, GraphqlMetadataGroup, GraphqlVideoAsset,
     GroupedCalendarEvent, ImportOrExportItemReviewComment, MediaAssociatedPersonStateChanges,
     MediaGeneralFilter, MediaSortBy, MetadataCreator, MetadataCreatorGroupedByRole,
-    MetadataDetails, MetadataFreeCreator, MetadataGroupsListInput, MetadataImage,
-    MetadataImageForMediaDetails, MetadataListInput, MetadataPartialDetails,
-    MetadataSearchItemResponse, MetadataVideo, MetadataVideoSource, PartialMetadata,
-    PartialMetadataWithoutId, PeopleListInput, PersonAndMetadataGroupsSortBy,
+    MetadataFreeCreator, MetadataGroupsListInput, MetadataImage, MetadataListInput,
+    MetadataPartialDetails, MetadataSearchItemResponse, MetadataVideo, MetadataVideoSource,
+    PartialMetadata, PartialMetadataWithoutId, PeopleListInput, PersonAndMetadataGroupsSortBy,
     PersonDetailsGroupedByRole, PersonDetailsItemWithCharacter, PodcastSpecifics,
     ProgressUpdateInput, ReviewPostedEvent, SeenAnimeExtraInformation, SeenPodcastExtraInformation,
-    SeenShowExtraInformation, ShowSpecifics, UpdateSeenItemInput, UserCalendarEventInput,
-    UserMediaNextEntry, UserMetadataDetailsEpisodeProgress, UserMetadataDetailsShowSeasonProgress,
-    UserUpcomingCalendarEventInput,
+    SeenShowExtraInformation, ShowSpecifics, UniqueMediaIdentifier, UpdateSeenItemInput,
+    UserCalendarEventInput, UserMediaNextEntry, UserMetadataDetailsEpisodeProgress,
+    UserMetadataDetailsShowSeasonProgress, UserUpcomingCalendarEventInput,
 };
 use migrations::{
     AliasedCalendarEvent, AliasedMetadata, AliasedMetadataToGenre, AliasedReview, AliasedSeen,
@@ -383,12 +382,8 @@ ORDER BY RANDOM() LIMIT 10;
         Ok(metadata)
     }
 
-    pub async fn deploy_update_metadata_job(
-        &self,
-        metadata_id: &String,
-        force_update: bool,
-    ) -> Result<bool> {
-        deploy_update_metadata_job(metadata_id, force_update, &self.0).await
+    pub async fn deploy_update_metadata_job(&self, metadata_id: &String) -> Result<bool> {
+        deploy_update_metadata_job(metadata_id, &self.0).await
     }
 
     pub async fn metadata_details(&self, metadata_id: &String) -> Result<GraphqlMetadataDetails> {
@@ -1055,7 +1050,9 @@ ORDER BY RANDOM() LIMIT 10;
         input: Vec<ProgressUpdateInput>,
     ) -> Result<bool> {
         self.0
-            .perform_core_application_job(CoreApplicationJob::BulkProgressUpdate(user_id, input))
+            .perform_application_job(ApplicationJob::Hp(
+                HpApplicationJob::BulkProgressUpdate(user_id, input),
+            ))
             .await?;
         Ok(true)
     }
@@ -1064,13 +1061,13 @@ ORDER BY RANDOM() LIMIT 10;
         &self,
         user_id: String,
         input: Vec<ProgressUpdateInput>,
-    ) -> Result<bool> {
+    ) -> Result<()> {
         for seen in input {
             progress_update(&user_id, false, seen, &self.0)
                 .await
                 .trace_ok();
         }
-        Ok(true)
+        Ok(())
     }
 
     pub async fn deploy_background_job(
@@ -1213,38 +1210,6 @@ ORDER BY RANDOM() LIMIT 10;
         Ok(())
     }
 
-    pub async fn commit_metadata_group_internal(
-        &self,
-        identifier: &String,
-        lot: MediaLot,
-        source: MediaSource,
-    ) -> Result<(String, Vec<PartialMetadataWithoutId>)> {
-        let existing_group = MetadataGroup::find()
-            .filter(metadata_group::Column::Identifier.eq(identifier))
-            .filter(metadata_group::Column::Lot.eq(lot))
-            .filter(metadata_group::Column::Source.eq(source))
-            .one(&self.0.db)
-            .await?;
-        let provider = get_metadata_provider(lot, source, &self.0).await?;
-        let (group_details, associated_items) = provider.metadata_group_details(identifier).await?;
-        let group_id = match existing_group {
-            Some(eg) => {
-                let mut eg: metadata_group::ActiveModel = eg.into();
-                eg.is_partial = ActiveValue::Set(Some(false));
-                let eg = eg.update(&self.0.db).await?;
-                eg.id
-            }
-            None => {
-                let mut db_group: metadata_group::ActiveModel =
-                    group_details.into_model("".to_string(), None).into();
-                db_group.id = ActiveValue::NotSet;
-                let new_group = db_group.insert(&self.0.db).await?;
-                new_group.id
-            }
-        };
-        Ok((group_id, associated_items))
-    }
-
     async fn create_partial_metadata(
         &self,
         data: PartialMetadataWithoutId,
@@ -1312,7 +1277,9 @@ ORDER BY RANDOM() LIMIT 10;
             .unwrap()
             .unwrap();
         self.0
-            .perform_application_job(ApplicationJob::UpdatePerson(person.id))
+            .perform_application_job(ApplicationJob::Mp(
+                MpApplicationJob::UpdatePerson(person.id),
+            ))
             .await?;
         Ok(true)
     }
@@ -1327,7 +1294,9 @@ ORDER BY RANDOM() LIMIT 10;
             .unwrap()
             .unwrap();
         self.0
-            .perform_application_job(ApplicationJob::UpdateMetadataGroup(metadata_group.id))
+            .perform_application_job(ApplicationJob::Mp(
+                MpApplicationJob::UpdateMetadataGroup(metadata_group.id),
+            ))
             .await?;
         Ok(true)
     }
@@ -1426,7 +1395,7 @@ ORDER BY RANDOM() LIMIT 10;
         Ok(true)
     }
 
-    pub async fn commit_metadata(&self, input: CommitMediaInput) -> Result<metadata::Model> {
+    pub async fn commit_metadata(&self, input: CommitMediaInput) -> Result<StringIdObject> {
         commit_metadata(input, &self.0).await
     }
 
@@ -1674,25 +1643,7 @@ ORDER BY RANDOM() LIMIT 10;
     }
 
     pub async fn commit_metadata_group(&self, input: CommitMediaInput) -> Result<StringIdObject> {
-        let (group_id, associated_items) =
-            commit_metadata_group_internal(&input.identifier, input.lot, input.source, &self.0)
-                .await?;
-        for (idx, media) in associated_items.into_iter().enumerate() {
-            let db_partial_metadata = create_partial_metadata(media, &self.0.db).await?;
-            MetadataToMetadataGroup::delete_many()
-                .filter(metadata_to_metadata_group::Column::MetadataGroupId.eq(&group_id))
-                .filter(metadata_to_metadata_group::Column::MetadataId.eq(&db_partial_metadata.id))
-                .exec(&self.0.db)
-                .await
-                .ok();
-            let intermediate = metadata_to_metadata_group::ActiveModel {
-                metadata_group_id: ActiveValue::Set(group_id.clone()),
-                metadata_id: ActiveValue::Set(db_partial_metadata.id),
-                part: ActiveValue::Set((idx + 1).try_into().unwrap()),
-            };
-            intermediate.insert(&self.0.db).await.ok();
-        }
-        Ok(StringIdObject { id: group_id })
+        commit_metadata_group(input, &self.0).await
     }
 
     pub async fn create_or_update_review(
@@ -1796,8 +1747,10 @@ ORDER BY RANDOM() LIMIT 10;
             .images
             .unwrap_or_default()
             .into_iter()
-            .map(|i| MetadataImageForMediaDetails { image: i })
-            .collect();
+            .map(|i| MetadataImage {
+                url: StoredUrl::S3(i),
+            })
+            .collect_vec();
         let videos = input
             .videos
             .unwrap_or_default()
@@ -1806,8 +1759,8 @@ ORDER BY RANDOM() LIMIT 10;
                 identifier: StoredUrl::S3(i),
                 source: MetadataVideoSource::Custom,
             })
-            .collect();
-        let creators = input
+            .collect_vec();
+        let free_creators = input
             .creators
             .unwrap_or_default()
             .into_iter()
@@ -1816,7 +1769,7 @@ ORDER BY RANDOM() LIMIT 10;
                 role: "Creator".to_string(),
                 image: None,
             })
-            .collect();
+            .collect_vec();
         let is_partial = match input.lot {
             MediaLot::Show => input.show_specifics.is_none(),
             MediaLot::Book => input.book_specifics.is_none(),
@@ -1829,43 +1782,62 @@ ORDER BY RANDOM() LIMIT 10;
             MediaLot::VideoGame => input.video_game_specifics.is_none(),
             MediaLot::VisualNovel => input.visual_novel_specifics.is_none(),
         };
-        let details = MetadataDetails {
-            videos,
-            creators,
-            identifier,
-            lot: input.lot,
-            title: input.title,
-            s3_images: images,
-            source: MediaSource::Custom,
-            description: input.description,
-            publish_year: input.publish_year,
-            show_specifics: input.show_specifics,
-            book_specifics: input.book_specifics,
-            manga_specifics: input.manga_specifics,
-            anime_specifics: input.anime_specifics,
-            movie_specifics: input.movie_specifics,
-            music_specifics: input.music_specifics,
-            genres: input.genres.unwrap_or_default(),
-            podcast_specifics: input.podcast_specifics,
-            audio_book_specifics: input.audio_book_specifics,
-            video_game_specifics: input.video_game_specifics,
-            visual_novel_specifics: input.visual_novel_specifics,
+        let metadata = metadata::ActiveModel {
+            lot: ActiveValue::Set(input.lot),
+            title: ActiveValue::Set(input.title),
+            identifier: ActiveValue::Set(identifier),
+            is_nsfw: ActiveValue::Set(input.is_nsfw),
+            source: ActiveValue::Set(MediaSource::Custom),
+            is_partial: ActiveValue::Set(Some(is_partial)),
+            description: ActiveValue::Set(input.description),
+            publish_year: ActiveValue::Set(input.publish_year),
+            show_specifics: ActiveValue::Set(input.show_specifics),
+            book_specifics: ActiveValue::Set(input.book_specifics),
+            manga_specifics: ActiveValue::Set(input.manga_specifics),
+            anime_specifics: ActiveValue::Set(input.anime_specifics),
+            movie_specifics: ActiveValue::Set(input.movie_specifics),
+            music_specifics: ActiveValue::Set(input.music_specifics),
+            podcast_specifics: ActiveValue::Set(input.podcast_specifics),
+            audio_book_specifics: ActiveValue::Set(input.audio_book_specifics),
+            video_game_specifics: ActiveValue::Set(input.video_game_specifics),
+            visual_novel_specifics: ActiveValue::Set(input.visual_novel_specifics),
+            images: ActiveValue::Set(match images.is_empty() {
+                true => None,
+                false => Some(images),
+            }),
+            videos: ActiveValue::Set(match videos.is_empty() {
+                true => None,
+                false => Some(videos),
+            }),
+            free_creators: ActiveValue::Set(match free_creators.is_empty() {
+                true => None,
+                false => Some(free_creators),
+            }),
             ..Default::default()
         };
-        let media = commit_metadata_internal(details, Some(is_partial), &self.0).await?;
+        let metadata = metadata.insert(&self.0.db).await?;
+        change_metadata_associations(
+            &metadata.id,
+            input.genres.unwrap_or_default(),
+            vec![],
+            vec![],
+            vec![],
+            &self.0,
+        )
+        .await?;
         add_entity_to_collection(
             &user_id,
             ChangeCollectionToEntityInput {
+                entity_id: metadata.id.clone(),
+                entity_lot: EntityLot::Metadata,
                 creator_user_id: user_id.to_owned(),
                 collection_name: DefaultCollection::Custom.to_string(),
-                entity_id: media.id.clone(),
-                entity_lot: EntityLot::Metadata,
                 ..Default::default()
             },
             &self.0,
         )
         .await?;
-        Ok(media)
+        Ok(metadata)
     }
 
     fn get_db_stmt(&self, stmt: SelectStatement) -> Statement {
@@ -1906,7 +1878,7 @@ ORDER BY RANDOM() LIMIT 10;
         for chunk in items {
             let promises = chunk
                 .into_iter()
-                .map(|m| self.update_metadata_and_notify_users(m, true));
+                .map(|m| self.update_metadata_and_notify_users(m));
             join_all(promises).await;
         }
         Ok(())
@@ -2214,7 +2186,7 @@ ORDER BY RANDOM() LIMIT 10;
         let mut group = MetadataGroup::find_by_id(metadata_group_id)
             .one(&self.0.db)
             .await?
-            .unwrap();
+            .ok_or_else(|| Error::new("Group not found"))?;
         let mut images = vec![];
         for image in group.images.clone().unwrap_or_default().iter() {
             images.push(
@@ -2267,7 +2239,6 @@ ORDER BY RANDOM() LIMIT 10;
                         )
                         .await?;
                         remove_entity_from_collection(
-                            &self.0.db,
                             &user.user_id,
                             ChangeCollectionToEntityInput {
                                 creator_user_id: col.user_id.clone(),
@@ -2276,6 +2247,7 @@ ORDER BY RANDOM() LIMIT 10;
                                 entity_lot: cte.entity_lot,
                                 ..Default::default()
                             },
+                            &self.0,
                         )
                         .await?;
                     }
@@ -2578,13 +2550,12 @@ ORDER BY RANDOM() LIMIT 10;
                 intermediate.insert(&self.0.db).await.unwrap();
             }
             let search_for = MediaAssociatedPersonStateChanges {
-                media: CommitMediaInput {
-                    identifier: pm.identifier.clone(),
+                role: data.role.clone(),
+                media: UniqueMediaIdentifier {
                     lot: pm.lot,
                     source: pm.source,
-                    ..Default::default()
+                    identifier: pm.identifier.clone(),
                 },
-                role: data.role.clone(),
             };
             if !default_state_changes.media_associated.contains(&search_for) {
                 notifications.push((
@@ -2602,12 +2573,8 @@ ORDER BY RANDOM() LIMIT 10;
         Ok(notifications)
     }
 
-    pub async fn update_metadata_and_notify_users(
-        &self,
-        metadata_id: &String,
-        force_update: bool,
-    ) -> Result<()> {
-        update_metadata_and_notify_users(metadata_id, force_update, &self.0).await
+    pub async fn update_metadata_and_notify_users(&self, metadata_id: &String) -> Result<()> {
+        update_metadata_and_notify_users(metadata_id, &self.0).await
     }
 
     pub async fn update_person_and_notify_users(&self, person_id: &String) -> Result<()> {
@@ -2638,17 +2605,36 @@ ORDER BY RANDOM() LIMIT 10;
     }
 
     pub async fn update_metadata_group(&self, metadata_group_id: &str) -> Result<()> {
-        let metadata_group = MetadataGroup::find_by_id(metadata_group_id)
+        let eg = MetadataGroup::find_by_id(metadata_group_id)
             .one(&self.0.db)
             .await?
-            .unwrap();
-        self.commit_metadata_group(CommitMediaInput {
-            lot: metadata_group.lot,
-            source: metadata_group.source,
-            identifier: metadata_group.identifier,
-            force_update: None,
-        })
-        .await?;
+            .ok_or(Error::new("Group not found"))?;
+        let provider = get_metadata_provider(eg.lot, eg.source, &self.0).await?;
+        let (group_details, associated_items) =
+            provider.metadata_group_details(&eg.identifier).await?;
+        let mut eg: metadata_group::ActiveModel = eg.into();
+        eg.is_partial = ActiveValue::Set(None);
+        eg.title = ActiveValue::Set(group_details.title);
+        eg.parts = ActiveValue::Set(group_details.parts);
+        eg.source_url = ActiveValue::Set(group_details.source_url);
+        eg.description = ActiveValue::Set(group_details.description);
+        eg.images = ActiveValue::Set(group_details.images.filter(|i| !i.is_empty()));
+        let eg = eg.update(&self.0.db).await?;
+        for (idx, media) in associated_items.into_iter().enumerate() {
+            let db_partial_metadata = create_partial_metadata(media, &self.0.db).await?;
+            MetadataToMetadataGroup::delete_many()
+                .filter(metadata_to_metadata_group::Column::MetadataGroupId.eq(&eg.id))
+                .filter(metadata_to_metadata_group::Column::MetadataId.eq(&db_partial_metadata.id))
+                .exec(&self.0.db)
+                .await
+                .ok();
+            let intermediate = metadata_to_metadata_group::ActiveModel {
+                metadata_group_id: ActiveValue::Set(eg.id.clone()),
+                metadata_id: ActiveValue::Set(db_partial_metadata.id),
+                part: ActiveValue::Set((idx + 1).try_into().unwrap()),
+            };
+            intermediate.insert(&self.0.db).await.ok();
+        }
         Ok(())
     }
 
@@ -2932,7 +2918,9 @@ ORDER BY RANDOM() LIMIT 10;
 
     pub async fn sync_integrations_data_to_owned_collection(&self) -> Result<()> {
         self.0
-            .perform_application_job(ApplicationJob::SyncIntegrationsData)
+            .perform_application_job(ApplicationJob::Mp(
+                MpApplicationJob::SyncIntegrationsData,
+            ))
             .await?;
         Ok(())
     }
diff --git a/crates/services/statistics/Cargo.toml b/crates/services/statistics/Cargo.toml
index ec9c1a6c1d..b461283de5 100644
--- a/crates/services/statistics/Cargo.toml
+++ b/crates/services/statistics/Cargo.toml
@@ -9,7 +9,7 @@ common-models = { path = "../../models/common" }
 database-models = { path = "../../models/database" }
 database-utils = { path = "../../utils/database" }
 dependent-models = { path = "../../models/dependent" }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 hashbag = { workspace = true }
 itertools = { workspace = true }
 sea-orm = { workspace = true }
diff --git a/crates/services/statistics/src/lib.rs b/crates/services/statistics/src/lib.rs
index d1a941ad66..76a234d54d 100644
--- a/crates/services/statistics/src/lib.rs
+++ b/crates/services/statistics/src/lib.rs
@@ -12,7 +12,7 @@ use dependent_models::{
     FitnessAnalyticsEquipment, FitnessAnalyticsExercise, FitnessAnalyticsMuscle, UserAnalytics,
     UserFitnessAnalytics,
 };
-use enums::{ExerciseEquipment, ExerciseMuscle};
+use enum_models::{ExerciseEquipment, ExerciseMuscle};
 use hashbag::HashBag;
 use itertools::Itertools;
 use sea_orm::{
diff --git a/crates/services/supporting/Cargo.toml b/crates/services/supporting/Cargo.toml
index 84a4e09e6f..e06a79522d 100644
--- a/crates/services/supporting/Cargo.toml
+++ b/crates/services/supporting/Cargo.toml
@@ -6,7 +6,7 @@ edition = "2021"
 [dependencies]
 async-graphql = { workspace = true }
 apalis = { workspace = true }
-background = { path = "../../background" }
+background-models = { path = "../../models/background" }
 cache-service = { path = "../../services/cache" }
 chrono = { workspace = true }
 chrono-tz = { workspace = true }
@@ -15,7 +15,7 @@ common-utils = { path = "../../utils/common" }
 config = { path = "../../config" }
 database-models = { path = "../../models/database" }
 dependent-models = { path = "../../models/dependent" }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 env-utils = { path = "../../utils/env" }
 file-storage-service = { path = "../../services/file-storage" }
 isolang = { workspace = true }
@@ -25,7 +25,6 @@ rustypipe = { workspace = true }
 sea-orm = { workspace = true }
 serde = { workspace = true }
 serde_json = { workspace = true }
-serde_with = { workspace = true }
 tracing = { workspace = true }
 unkey = { workspace = true }
 
diff --git a/crates/services/supporting/src/lib.rs b/crates/services/supporting/src/lib.rs
index 8b9d01be85..889564f0c6 100644
--- a/crates/services/supporting/src/lib.rs
+++ b/crates/services/supporting/src/lib.rs
@@ -2,7 +2,10 @@ use std::sync::Arc;
 
 use apalis::prelude::{MemoryStorage, MessageQueue};
 use async_graphql::Result;
-use background::{ApplicationJob, CoreApplicationJob};
+use background_models::{
+    ApplicationJob, HpApplicationJob, LpApplicationJob,
+    MpApplicationJob,
+};
 use cache_service::CacheService;
 use chrono::{NaiveDate, TimeZone, Utc};
 use common_models::{ApplicationCacheKey, BackendError};
@@ -15,7 +18,7 @@ use dependent_models::{
     ApplicationCacheValue, CoreDetails, ExerciseFilters, ExerciseParameters,
     ExerciseParametersLotMapping, MetadataLotSourceMappings, ProviderLanguageInformation,
 };
-use enums::{
+use enum_models::{
     ExerciseEquipment, ExerciseForce, ExerciseLevel, ExerciseLot, ExerciseMechanic, ExerciseMuscle,
     MediaSource,
 };
@@ -26,7 +29,6 @@ use openidconnect::core::CoreClient;
 use rustypipe::param::{Language, LANGUAGES};
 use sea_orm::{DatabaseConnection, EntityTrait, Iterable, PaginatorTrait};
 use serde::{Deserialize, Serialize};
-use serde_with::skip_serializing_none;
 use unkey::{models::VerifyKeyRequest, Client};
 
 pub struct SupportingService {
@@ -37,8 +39,9 @@ pub struct SupportingService {
     pub oidc_client: Option<CoreClient>,
     pub file_storage_service: Arc<FileStorageService>,
 
-    perform_application_job: MemoryStorage<ApplicationJob>,
-    perform_core_application_job: MemoryStorage<CoreApplicationJob>,
+    lp_application_job: MemoryStorage<LpApplicationJob>,
+    hp_application_job: MemoryStorage<HpApplicationJob>,
+    mp_application_job: MemoryStorage<MpApplicationJob>,
 }
 
 impl SupportingService {
@@ -50,8 +53,9 @@ impl SupportingService {
         config: Arc<config::AppConfig>,
         oidc_client: Option<CoreClient>,
         file_storage_service: Arc<FileStorageService>,
-        perform_application_job: &MemoryStorage<ApplicationJob>,
-        perform_core_application_job: &MemoryStorage<CoreApplicationJob>,
+        lp_application_job: &MemoryStorage<LpApplicationJob>,
+        mp_application_job: &MemoryStorage<MpApplicationJob>,
+        hp_application_job: &MemoryStorage<HpApplicationJob>,
     ) -> Self {
         Self {
             config,
@@ -60,26 +64,24 @@ impl SupportingService {
             cache_service,
             db: db.clone(),
             file_storage_service,
-            perform_application_job: perform_application_job.clone(),
-            perform_core_application_job: perform_core_application_job.clone(),
+            lp_application_job: lp_application_job.clone(),
+            mp_application_job: mp_application_job.clone(),
+            hp_application_job: hp_application_job.clone(),
         }
     }
 
     pub async fn perform_application_job(&self, job: ApplicationJob) -> Result<()> {
-        self.perform_application_job
-            .clone()
-            .enqueue(job)
-            .await
-            .unwrap();
-        Ok(())
-    }
-
-    pub async fn perform_core_application_job(&self, job: CoreApplicationJob) -> Result<()> {
-        self.perform_core_application_job
-            .clone()
-            .enqueue(job)
-            .await
-            .unwrap();
+        match job {
+            ApplicationJob::Lp(job) => {
+                self.lp_application_job.clone().enqueue(job).await.ok();
+            }
+            ApplicationJob::Hp(job) => {
+                self.hp_application_job.clone().enqueue(job).await.ok();
+            }
+            ApplicationJob::Mp(job) => {
+                self.mp_application_job.clone().enqueue(job).await.ok();
+            }
+        }
         Ok(())
     }
 
@@ -90,7 +92,6 @@ impl SupportingService {
         }
         ryot_log!(debug, "Verifying pro key for API ID: {:#?}", UNKEY_API_ID);
         let compile_timestamp = Utc.timestamp_opt(COMPILATION_TIMESTAMP, 0).unwrap();
-        #[skip_serializing_none]
         #[derive(Debug, Serialize, Clone, Deserialize)]
         struct Meta {
             expiry: Option<NaiveDate>,
@@ -195,45 +196,21 @@ impl SupportingService {
                                 .collect(),
                             "us".to_owned(),
                         ),
-                        MediaSource::Openlibrary => (
-                            ["us"].into_iter().map(String::from).collect(),
-                            "us".to_owned(),
-                        ),
                         MediaSource::Tmdb => (
                             isolang::languages()
                                 .filter_map(|l| l.to_639_1().map(String::from))
                                 .collect(),
                             "en".to_owned(),
                         ),
-                        MediaSource::Listennotes => (
-                            ["us"].into_iter().map(String::from).collect(),
-                            "us".to_owned(),
-                        ),
-                        MediaSource::GoogleBooks => (vec!["us".to_owned()], "us".to_owned()),
-                        MediaSource::Igdb => (
-                            ["us"].into_iter().map(String::from).collect(),
-                            "us".to_owned(),
-                        ),
-                        MediaSource::MangaUpdates => (
-                            ["us"].into_iter().map(String::from).collect(),
-                            "us".to_owned(),
-                        ),
-                        MediaSource::Anilist => (
-                            ["us"].into_iter().map(String::from).collect(),
-                            "us".to_owned(),
-                        ),
-                        MediaSource::Mal => (
-                            ["us"].into_iter().map(String::from).collect(),
-                            "us".to_owned(),
-                        ),
-                        MediaSource::Custom => (
-                            ["us"].into_iter().map(String::from).collect(),
-                            "us".to_owned(),
-                        ),
-                        MediaSource::Vndb => (
-                            ["us"].into_iter().map(String::from).collect(),
-                            "us".to_owned(),
-                        ),
+                        MediaSource::GoogleBooks
+                        | MediaSource::Listennotes
+                        | MediaSource::Openlibrary
+                        | MediaSource::Igdb
+                        | MediaSource::MangaUpdates
+                        | MediaSource::Anilist
+                        | MediaSource::Mal
+                        | MediaSource::Custom
+                        | MediaSource::Vndb => (vec!["us".to_owned()], "us".to_owned()),
                     };
                     ProviderLanguageInformation {
                         source,
diff --git a/crates/services/user/Cargo.toml b/crates/services/user/Cargo.toml
index 5771ccfcef..57c7d44656 100644
--- a/crates/services/user/Cargo.toml
+++ b/crates/services/user/Cargo.toml
@@ -13,8 +13,9 @@ common-utils = { path = "../../utils/common" }
 database-models = { path = "../../models/database" }
 database-utils = { path = "../../utils/database" }
 dependent-models = { path = "../../models/dependent" }
+dependent-utils = { path = "../../utils/dependent" }
 enum_meta = { workspace = true }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 fitness-models = { path = "../../models/fitness" }
 itertools = { workspace = true }
 jwt-service = { path = "../../services/jwt" }
diff --git a/crates/services/user/src/lib.rs b/crates/services/user/src/lib.rs
index 47e15e6cec..b37d722cea 100644
--- a/crates/services/user/src/lib.rs
+++ b/crates/services/user/src/lib.rs
@@ -12,13 +12,13 @@ use database_models::{
     user,
 };
 use database_utils::{
-    admin_account_guard, create_or_update_collection,
-    deploy_job_to_calculate_user_activities_and_summary, ilike_sql, revoke_access_link,
-    server_key_validation_guard, user_by_id,
+    admin_account_guard, deploy_job_to_calculate_user_activities_and_summary, ilike_sql,
+    revoke_access_link, server_key_validation_guard, user_by_id,
 };
 use dependent_models::UserDetailsResult;
+use dependent_utils::create_or_update_collection;
 use enum_meta::Meta;
-use enums::{IntegrationLot, IntegrationProvider, NotificationPlatformLot, UserLot};
+use enum_models::{IntegrationLot, IntegrationProvider, NotificationPlatformLot, UserLot};
 use fitness_models::UserUnitSystem;
 use itertools::Itertools;
 use jwt_service::{sign, AccessLinkClaims};
@@ -290,7 +290,6 @@ impl UserService {
         for col in DefaultCollection::iter() {
             let meta = col.meta().to_owned();
             create_or_update_collection(
-                &self.0.db,
                 &user.id,
                 CreateOrUpdateCollectionInput {
                     name: col.to_string(),
@@ -298,6 +297,7 @@ impl UserService {
                     information_template: meta.0,
                     ..Default::default()
                 },
+                &self.0,
             )
             .await
             .ok();
diff --git a/crates/traits/src/lib.rs b/crates/traits/src/lib.rs
index 32939e877a..988a8d6825 100644
--- a/crates/traits/src/lib.rs
+++ b/crates/traits/src/lib.rs
@@ -10,7 +10,7 @@ use database_models::metadata_group::MetadataGroupWithoutId;
 use database_utils::check_token;
 use dependent_models::{MetadataGroupSearchResponse, PeopleSearchResponse, SearchResults};
 use media_models::{MetadataDetails, MetadataPerson, MetadataSearchItem, PartialMetadataWithoutId};
-use sea_orm::{prelude::DateTimeUtc, DatabaseConnection};
+use sea_orm::DatabaseConnection;
 
 #[async_trait]
 pub trait MediaProvider {
@@ -31,12 +31,6 @@ pub trait MediaProvider {
         bail!("This provider does not support getting media details")
     }
 
-    /// Get whether a metadata has been updated since the given date.
-    #[allow(unused_variables)]
-    async fn metadata_updated_since(&self, identifier: &str, since: DateTimeUtc) -> Result<bool> {
-        bail!("This provider does not support checking if metadata has been updated")
-    }
-
     /// Search for people via a query.
     #[allow(unused_variables)]
     async fn people_search(
diff --git a/crates/utils/common/Cargo.toml b/crates/utils/common/Cargo.toml
index 91a75e98dd..c967903293 100644
--- a/crates/utils/common/Cargo.toml
+++ b/crates/utils/common/Cargo.toml
@@ -7,7 +7,7 @@ edition = "2021"
 chrono = { workspace = true }
 compile-time = { workspace = true }
 const-str = { workspace = true }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 env-utils = { path = "../env" }
 reqwest = { workspace = true }
 serde = { workspace = true }
diff --git a/crates/utils/common/src/lib.rs b/crates/utils/common/src/lib.rs
index afb5859f3b..c1e1f538e3 100644
--- a/crates/utils/common/src/lib.rs
+++ b/crates/utils/common/src/lib.rs
@@ -1,7 +1,7 @@
 use std::{convert::TryInto, fmt};
 
 use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
-use enums::{ExerciseLot, MediaLot, MediaSource, WorkoutSetPersonalBest};
+use enum_models::{ExerciseLot, MediaLot, MediaSource, WorkoutSetPersonalBest};
 use env_utils::APP_VERSION;
 use reqwest::header::HeaderValue;
 use serde::de;
diff --git a/crates/utils/database/Cargo.toml b/crates/utils/database/Cargo.toml
index 9937a07ea8..025c911734 100644
--- a/crates/utils/database/Cargo.toml
+++ b/crates/utils/database/Cargo.toml
@@ -6,13 +6,13 @@ edition = "2021"
 [dependencies]
 application-utils = { path = "../../utils/application" }
 async-graphql = { workspace = true }
-background = { path = "../../background" }
+background-models = { path = "../../models/background" }
 chrono = { workspace = true }
 common-models = { path = "../../models/common" }
 common-utils = { path = "../../utils/common" }
 database-models = { path = "../../models/database" }
 dependent-models = { path = "../../models/dependent" }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 fitness-models = { path = "../../models/fitness" }
 futures = { workspace = true }
 itertools = { workspace = true }
diff --git a/crates/utils/database/src/lib.rs b/crates/utils/database/src/lib.rs
index f6ecb9d4e7..e16ba14695 100644
--- a/crates/utils/database/src/lib.rs
+++ b/crates/utils/database/src/lib.rs
@@ -4,17 +4,14 @@ use application_utils::{
     get_podcast_episode_by_number, get_show_episode_by_numbers, GraphqlRepresentation,
 };
 use async_graphql::{Error, Result};
-use background::ApplicationJob;
-use chrono::{Timelike, Utc};
+use background_models::{ApplicationJob, HpApplicationJob};
+use chrono::Timelike;
 use common_models::{
-    BackendError, ChangeCollectionToEntityInput, DailyUserActivityHourRecord,
-    DailyUserActivityHourRecordEntity, DefaultCollection, IdAndNamedObject, StringIdObject,
+    BackendError, DailyUserActivityHourRecord, DailyUserActivityHourRecordEntity, IdAndNamedObject,
 };
 use common_utils::ryot_log;
 use database_models::{
-    access_link, collection, collection_to_entity, daily_user_activity,
-    functions::associate_user_with_entity,
-    metadata,
+    access_link, collection, collection_to_entity, daily_user_activity, metadata,
     prelude::{
         AccessLink, Collection, CollectionToEntity, DailyUserActivity, Exercise, Metadata, Review,
         Seen, User, UserMeasurement, UserToEntity, Workout, WorkoutTemplate,
@@ -22,26 +19,26 @@ use database_models::{
     review, seen, user, user_measurement, user_to_entity, workout,
 };
 use dependent_models::{UserWorkoutDetails, UserWorkoutTemplateDetails};
-use enums::{EntityLot, MediaLot, SeenState, UserLot, Visibility};
+use enum_models::{EntityLot, MediaLot, SeenState, UserLot, Visibility};
 use fitness_models::UserMeasurementsListInput;
 use futures::TryStreamExt;
 use itertools::Itertools;
 use jwt_service::{verify, Claims};
 use markdown::to_html as markdown_to_html;
 use media_models::{
-    AnimeSpecifics, AudioBookSpecifics, BookSpecifics, CreateOrUpdateCollectionInput,
-    MangaSpecifics, MovieSpecifics, MusicSpecifics, PodcastSpecifics, ReviewItem,
-    SeenAnimeExtraInformation, SeenMangaExtraInformation, SeenPodcastExtraInformation,
-    SeenShowExtraInformation, ShowSpecifics, VideoGameSpecifics, VisualNovelSpecifics,
+    AnimeSpecifics, AudioBookSpecifics, BookSpecifics, MangaSpecifics, MovieSpecifics,
+    MusicSpecifics, PodcastSpecifics, ReviewItem, SeenAnimeExtraInformation,
+    SeenMangaExtraInformation, SeenPodcastExtraInformation, SeenShowExtraInformation,
+    ShowSpecifics, VideoGameSpecifics, VisualNovelSpecifics,
 };
 use migrations::AliasedCollectionToEntity;
 use rust_decimal::{prelude::ToPrimitive, Decimal};
 use rust_decimal_macros::dec;
 use sea_orm::{
     prelude::{Date, DateTimeUtc, Expr},
-    sea_query::{NullOrdering, OnConflict, PgFunc},
+    sea_query::{NullOrdering, PgFunc},
     ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, FromQueryResult,
-    Iterable, Order, QueryFilter, QueryOrder, QuerySelect, QueryTrait, Select, TransactionTrait,
+    Order, QueryFilter, QueryOrder, QuerySelect, QueryTrait, Select,
 };
 use serde::{Deserialize, Serialize};
 use supporting_service::SupportingService;
@@ -289,31 +286,6 @@ pub async fn check_token(
     Ok(true)
 }
 
-pub async fn remove_entity_from_collection(
-    db: &DatabaseConnection,
-    user_id: &String,
-    input: ChangeCollectionToEntityInput,
-) -> Result<StringIdObject> {
-    let collect = Collection::find()
-        .left_join(UserToEntity)
-        .filter(collection::Column::Name.eq(input.collection_name))
-        .filter(user_to_entity::Column::UserId.eq(input.creator_user_id))
-        .one(db)
-        .await
-        .unwrap()
-        .unwrap();
-    let column = get_cte_column_from_lot(input.entity_lot);
-    CollectionToEntity::delete_many()
-        .filter(collection_to_entity::Column::CollectionId.eq(collect.id.clone()))
-        .filter(column.eq(input.entity_id.clone()))
-        .exec(db)
-        .await?;
-    if input.entity_lot != EntityLot::Workout && input.entity_lot != EntityLot::WorkoutTemplate {
-        associate_user_with_entity(db, user_id, &input.entity_id, input.entity_lot).await?;
-    }
-    Ok(StringIdObject { id: collect.id })
-}
-
 pub async fn item_reviews(
     user_id: &String,
     entity_id: &String,
@@ -403,74 +375,6 @@ pub async fn item_reviews(
     Ok(all_reviews)
 }
 
-pub async fn create_or_update_collection(
-    db: &DatabaseConnection,
-    user_id: &String,
-    input: CreateOrUpdateCollectionInput,
-) -> Result<StringIdObject> {
-    let txn = db.begin().await?;
-    let meta = Collection::find()
-        .filter(collection::Column::Name.eq(input.name.clone()))
-        .filter(collection::Column::UserId.eq(user_id))
-        .one(&txn)
-        .await
-        .unwrap();
-    let mut new_name = input.name.clone();
-    let created = match meta {
-        Some(m) if input.update_id.is_none() => m.id,
-        _ => {
-            let col = collection::ActiveModel {
-                id: match input.update_id {
-                    Some(i) => {
-                        let already = Collection::find_by_id(i.clone())
-                            .one(&txn)
-                            .await
-                            .unwrap()
-                            .unwrap();
-                        if DefaultCollection::iter()
-                            .map(|s| s.to_string())
-                            .contains(&already.name)
-                        {
-                            new_name = already.name;
-                        }
-                        ActiveValue::Unchanged(i.clone())
-                    }
-                    None => ActiveValue::NotSet,
-                },
-                last_updated_on: ActiveValue::Set(Utc::now()),
-                name: ActiveValue::Set(new_name),
-                user_id: ActiveValue::Set(user_id.to_owned()),
-                description: ActiveValue::Set(input.description),
-                information_template: ActiveValue::Set(input.information_template),
-                ..Default::default()
-            };
-            let inserted = col
-                .save(&txn)
-                .await
-                .map_err(|_| Error::new("There was an error creating the collection".to_owned()))?;
-            let id = inserted.id.unwrap();
-            let mut collaborators = vec![user_id.to_owned()];
-            if let Some(input_collaborators) = input.collaborators {
-                collaborators.extend(input_collaborators);
-            }
-            let inserts = collaborators
-                .into_iter()
-                .map(|c| user_to_entity::ActiveModel {
-                    user_id: ActiveValue::Set(c),
-                    collection_id: ActiveValue::Set(Some(id.clone())),
-                    ..Default::default()
-                });
-            UserToEntity::insert_many(inserts)
-                .on_conflict(OnConflict::new().do_nothing().to_owned())
-                .exec_without_returning(&txn)
-                .await?;
-            id
-        }
-    };
-    txn.commit().await?;
-    Ok(StringIdObject { id: created })
-}
-
 pub async fn calculate_user_activities_and_summary(
     db: &DatabaseConnection,
     user_id: &String,
@@ -816,9 +720,11 @@ pub async fn deploy_job_to_calculate_user_activities_and_summary(
     calculate_from_beginning: bool,
     ss: &Arc<SupportingService>,
 ) {
-    ss.perform_application_job(ApplicationJob::RecalculateUserActivitiesAndSummary(
-        user_id.to_owned(),
-        calculate_from_beginning,
+    ss.perform_application_job(ApplicationJob::Hp(
+        HpApplicationJob::RecalculateUserActivitiesAndSummary(
+            user_id.to_owned(),
+            calculate_from_beginning,
+        ),
     ))
     .await
     .unwrap();
diff --git a/crates/utils/dependent/Cargo.toml b/crates/utils/dependent/Cargo.toml
index 66681ab26a..47244a077b 100644
--- a/crates/utils/dependent/Cargo.toml
+++ b/crates/utils/dependent/Cargo.toml
@@ -6,7 +6,7 @@ edition = "2021"
 [dependencies]
 application-utils = { path = "../application" }
 async-graphql = { workspace = true }
-background = { path = "../../background" }
+background-models = { path = "../../models/background" }
 chrono = { workspace = true }
 common-models = { path = "../../models/common" }
 common-utils = { path = "../common" }
@@ -14,7 +14,7 @@ config = { path = "../../config" }
 database-models = { path = "../../models/database" }
 database-utils = { path = "../database" }
 dependent-models = { path = "../../models/dependent" }
-enums = { path = "../../enums" }
+enum-models = { path = "../../models/enum" }
 file-storage-service = { path = "../../services/file-storage" }
 fitness-models = { path = "../../models/fitness" }
 importer-models = { path = "../../models/importer" }
diff --git a/crates/utils/dependent/src/lib.rs b/crates/utils/dependent/src/lib.rs
index f574f2e2ba..bda8ee2073 100644
--- a/crates/utils/dependent/src/lib.rs
+++ b/crates/utils/dependent/src/lib.rs
@@ -1,8 +1,14 @@
-use std::{cmp::Reverse, collections::HashMap, future::Future, iter::zip, sync::Arc};
+use std::{
+    cmp::Reverse,
+    collections::{HashMap, HashSet},
+    future::Future,
+    iter::zip,
+    sync::Arc,
+};
 
 use application_utils::get_current_date;
 use async_graphql::{Enum, Error, Result};
-use background::{ApplicationJob, CoreApplicationJob};
+use background_models::{ApplicationJob, HpApplicationJob, LpApplicationJob, MpApplicationJob};
 use chrono::Utc;
 use common_models::{
     ApplicationCacheKey, BackgroundJob, ChangeCollectionToEntityInput, DefaultCollection,
@@ -13,8 +19,8 @@ use common_utils::{ryot_log, EXERCISE_LOT_MAPPINGS, SHOW_SPECIAL_SEASON_NAMES};
 use database_models::{
     collection, collection_to_entity, exercise,
     functions::associate_user_with_entity,
-    genre, metadata, metadata_group, metadata_to_genre, metadata_to_metadata, metadata_to_person,
-    monitored_entity, person,
+    genre, metadata, metadata_group, metadata_to_genre, metadata_to_metadata,
+    metadata_to_metadata_group, metadata_to_person, monitored_entity, person,
     prelude::{
         Collection, CollectionToEntity, Exercise, Genre, Metadata, MetadataGroup, MetadataToGenre,
         MetadataToMetadata, MetadataToPerson, MonitoredEntity, Person, Seen, UserToEntity, Workout,
@@ -22,11 +28,10 @@ use database_models::{
     review, seen, user_measurement, user_notification, user_to_entity, workout,
 };
 use database_utils::{
-    admin_account_guard, create_or_update_collection, get_cte_column_from_lot,
-    remove_entity_from_collection, schedule_user_for_workout_revision, user_by_id,
+    admin_account_guard, get_cte_column_from_lot, schedule_user_for_workout_revision, user_by_id,
 };
 use dependent_models::{ApplicationCacheValue, EmptyCacheValue, ImportCompletedItem, ImportResult};
-use enums::{
+use enum_models::{
     EntityLot, ExerciseLot, ExerciseSource, MediaLot, MediaSource, MetadataToMetadataRelation,
     SeenState, UserNotificationLot, Visibility, WorkoutSetPersonalBest,
 };
@@ -49,7 +54,7 @@ use media_models::{
     ProgressUpdateErrorVariant, ProgressUpdateInput, ProgressUpdateResultUnion, ReviewPostedEvent,
     SeenAnimeExtraInformation, SeenMangaExtraInformation, SeenPodcastExtraInformation,
     SeenPodcastExtraOptionalInformation, SeenShowExtraInformation,
-    SeenShowExtraOptionalInformation,
+    SeenShowExtraOptionalInformation, UniqueMediaIdentifier,
 };
 use nanoid::nanoid;
 use providers::{
@@ -73,8 +78,9 @@ use rust_decimal::{
 use rust_decimal_macros::dec;
 use sea_orm::{
     prelude::{DateTimeUtc, Expr},
-    ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait,
-    QueryFilter, QueryOrder, QuerySelect, QueryTrait,
+    sea_query::OnConflict,
+    ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, Iterable,
+    ModelTrait, QueryFilter, QueryOrder, QuerySelect, QueryTrait, TransactionTrait,
 };
 use serde::{Deserialize, Serialize};
 use supporting_service::SupportingService;
@@ -179,35 +185,6 @@ pub async fn details_from_provider(
     Ok(results)
 }
 
-pub async fn commit_person(
-    input: CommitPersonInput,
-    db: &DatabaseConnection,
-) -> Result<StringIdObject> {
-    if let Some(p) = Person::find()
-        .filter(person::Column::Source.eq(input.source))
-        .filter(person::Column::Identifier.eq(input.identifier.clone()))
-        .apply_if(input.source_specifics.clone(), |query, v| {
-            query.filter(person::Column::SourceSpecifics.eq(v))
-        })
-        .one(db)
-        .await?
-        .map(|p| StringIdObject { id: p.id })
-    {
-        Ok(p)
-    } else {
-        let person = person::ActiveModel {
-            identifier: ActiveValue::Set(input.identifier),
-            source: ActiveValue::Set(input.source),
-            source_specifics: ActiveValue::Set(input.source_specifics),
-            name: ActiveValue::Set(input.name),
-            is_partial: ActiveValue::Set(Some(true)),
-            ..Default::default()
-        };
-        let person = person.insert(db).await?;
-        Ok(StringIdObject { id: person.id })
-    }
-}
-
 async fn associate_person_with_metadata(
     metadata_id: &str,
     person: PartialMetadataPerson,
@@ -217,20 +194,20 @@ async fn associate_person_with_metadata(
     let role = person.role.clone();
     let db_person = commit_person(
         CommitPersonInput {
-            identifier: person.identifier.clone(),
+            name: person.name,
             source: person.source,
+            identifier: person.identifier.clone(),
             source_specifics: person.source_specifics,
-            name: person.name,
         },
         db,
     )
     .await?;
     let intermediate = metadata_to_person::ActiveModel {
-        metadata_id: ActiveValue::Set(metadata_id.to_owned()),
-        person_id: ActiveValue::Set(db_person.id),
         role: ActiveValue::Set(role),
-        index: ActiveValue::Set(Some(index.try_into().unwrap())),
+        person_id: ActiveValue::Set(db_person.id),
         character: ActiveValue::Set(person.character),
+        metadata_id: ActiveValue::Set(metadata_id.to_owned()),
+        index: ActiveValue::Set(Some(index.try_into().unwrap())),
     };
     intermediate.insert(db).await.ok();
     Ok(())
@@ -256,8 +233,8 @@ async fn associate_genre_with_metadata(
         c.insert(db).await.unwrap()
     };
     let intermediate = metadata_to_genre::ActiveModel {
-        metadata_id: ActiveValue::Set(metadata_id.to_owned()),
         genre_id: ActiveValue::Set(db_genre.id),
+        metadata_id: ActiveValue::Set(metadata_id.to_owned()),
     };
     intermediate.insert(db).await.ok();
     Ok(())
@@ -313,8 +290,8 @@ async fn associate_suggestion_with_metadata(
 ) -> Result<()> {
     let db_partial_metadata = create_partial_metadata(data, db).await?;
     let intermediate = metadata_to_metadata::ActiveModel {
-        from_metadata_id: ActiveValue::Set(metadata_id.to_owned()),
         to_metadata_id: ActiveValue::Set(db_partial_metadata.id),
+        from_metadata_id: ActiveValue::Set(metadata_id.to_owned()),
         relation: ActiveValue::Set(MetadataToMetadataRelation::Suggestion),
         ..Default::default()
     };
@@ -322,26 +299,26 @@ async fn associate_suggestion_with_metadata(
     Ok(())
 }
 
-async fn deploy_associate_group_with_metadata_job(
-    lot: MediaLot,
-    source: MediaSource,
-    identifier: String,
+async fn associate_metadata_group_with_metadata(
+    metadata_id: &String,
+    metadata_group: CommitMediaInput,
     ss: &Arc<SupportingService>,
 ) -> Result<()> {
-    ss.perform_application_job(ApplicationJob::AssociateGroupWithMetadata(
-        lot, source, identifier,
-    ))
-    .await
+    let db_group = commit_metadata_group(metadata_group, ss).await?;
+    let intermediate = metadata_to_metadata_group::ActiveModel {
+        part: ActiveValue::Set(0),
+        metadata_group_id: ActiveValue::Set(db_group.id),
+        metadata_id: ActiveValue::Set(metadata_id.to_owned()),
+    };
+    intermediate.insert(&ss.db).await.ok();
+    Ok(())
 }
 
-#[allow(clippy::too_many_arguments)]
-async fn change_metadata_associations(
+pub async fn change_metadata_associations(
     metadata_id: &String,
-    lot: MediaLot,
-    source: MediaSource,
     genres: Vec<String>,
     suggestions: Vec<PartialMetadataWithoutId>,
-    groups: Vec<String>,
+    groups: Vec<CommitMediaInput>,
     people: Vec<PartialMetadataPerson>,
     ss: &Arc<SupportingService>,
 ) -> Result<()> {
@@ -373,8 +350,8 @@ async fn change_metadata_associations(
             .await
             .ok();
     }
-    for group_identifier in groups {
-        deploy_associate_group_with_metadata_job(lot, source, group_identifier, ss)
+    for group in groups {
+        associate_metadata_group_with_metadata(metadata_id, group, ss)
             .await
             .ok();
     }
@@ -383,7 +360,6 @@ async fn change_metadata_associations(
 
 pub async fn update_metadata(
     metadata_id: &String,
-    force_update: bool,
     ss: &Arc<SupportingService>,
 ) -> Result<Vec<(String, MediaStateChanged)>> {
     let metadata = Metadata::find_by_id(metadata_id)
@@ -391,21 +367,6 @@ pub async fn update_metadata(
         .await
         .unwrap()
         .unwrap();
-    if !force_update {
-        // check whether the metadata needs to be updated
-        let provider = get_metadata_provider(metadata.lot, metadata.source, ss).await?;
-        if let Ok(false) = provider
-            .metadata_updated_since(&metadata.identifier, metadata.last_updated_on)
-            .await
-        {
-            ryot_log!(
-                debug,
-                "Metadata {:?} does not need to be updated",
-                metadata_id
-            );
-            return Ok(vec![]);
-        }
-    }
     ryot_log!(debug, "Updating metadata for {:?}", metadata_id);
     Metadata::update_many()
         .filter(metadata::Column::Id.eq(metadata_id))
@@ -624,11 +585,9 @@ pub async fn update_metadata(
 
             change_metadata_associations(
                 &metadata.id,
-                metadata.lot,
-                metadata.source,
                 details.genres,
                 details.suggestions,
-                details.group_identifiers,
+                details.groups,
                 details.people,
                 ss,
             )
@@ -738,12 +697,9 @@ pub async fn refresh_collection_to_entity_association(
 
 pub async fn update_metadata_and_notify_users(
     metadata_id: &String,
-    force_update: bool,
     ss: &Arc<SupportingService>,
 ) -> Result<()> {
-    let notifications = update_metadata(metadata_id, force_update, ss)
-        .await
-        .unwrap();
+    let notifications = update_metadata(metadata_id, ss).await?;
     if !notifications.is_empty() {
         let users_to_notify =
             get_users_and_cte_monitoring_entity(metadata_id, EntityLot::Metadata, &ss.db).await?;
@@ -761,110 +717,100 @@ pub async fn update_metadata_and_notify_users(
     Ok(())
 }
 
-pub async fn commit_metadata_internal(
-    details: MetadataDetails,
-    is_partial: Option<bool>,
+pub async fn commit_metadata_group(
+    input: CommitMediaInput,
     ss: &Arc<SupportingService>,
-) -> Result<metadata::Model> {
-    let mut images = vec![];
-    images.extend(details.url_images.into_iter().map(|i| MetadataImage {
-        url: StoredUrl::Url(i.image),
-    }));
-    images.extend(details.s3_images.into_iter().map(|i| MetadataImage {
-        url: StoredUrl::S3(i.image),
-    }));
-    let metadata = metadata::ActiveModel {
-        lot: ActiveValue::Set(details.lot),
-        source: ActiveValue::Set(details.source),
-        title: ActiveValue::Set(details.title),
-        source_url: ActiveValue::Set(details.source_url),
-        description: ActiveValue::Set(details.description),
-        publish_year: ActiveValue::Set(details.publish_year),
-        publish_date: ActiveValue::Set(details.publish_date),
-        images: ActiveValue::Set(match images.is_empty() {
-            true => None,
-            false => Some(images),
-        }),
-        videos: ActiveValue::Set(match details.videos.is_empty() {
-            true => None,
-            false => Some(details.videos),
-        }),
-        identifier: ActiveValue::Set(details.identifier),
-        audio_book_specifics: ActiveValue::Set(details.audio_book_specifics),
-        anime_specifics: ActiveValue::Set(details.anime_specifics),
-        book_specifics: ActiveValue::Set(details.book_specifics),
-        manga_specifics: ActiveValue::Set(details.manga_specifics),
-        movie_specifics: ActiveValue::Set(details.movie_specifics),
-        podcast_specifics: ActiveValue::Set(details.podcast_specifics),
-        show_specifics: ActiveValue::Set(details.show_specifics),
-        video_game_specifics: ActiveValue::Set(details.video_game_specifics),
-        visual_novel_specifics: ActiveValue::Set(details.visual_novel_specifics),
-        provider_rating: ActiveValue::Set(details.provider_rating),
-        production_status: ActiveValue::Set(details.production_status),
-        music_specifics: ActiveValue::Set(details.music_specifics),
-        original_language: ActiveValue::Set(details.original_language),
-        external_identifiers: ActiveValue::Set(details.external_identifiers),
-        is_nsfw: ActiveValue::Set(details.is_nsfw),
-        is_partial: ActiveValue::Set(is_partial),
-        free_creators: ActiveValue::Set(if details.creators.is_empty() {
-            None
-        } else {
-            Some(details.creators)
-        }),
-        watch_providers: ActiveValue::Set(if details.watch_providers.is_empty() {
-            None
-        } else {
-            Some(details.watch_providers)
-        }),
-        ..Default::default()
-    };
-    let metadata = metadata.insert(&ss.db).await?;
-
-    change_metadata_associations(
-        &metadata.id,
-        metadata.lot,
-        metadata.source,
-        details.genres.clone(),
-        details.suggestions.clone(),
-        details.group_identifiers.clone(),
-        details.people.clone(),
-        ss,
-    )
-    .await?;
-    Ok(metadata)
+) -> Result<StringIdObject> {
+    match MetadataGroup::find()
+        .filter(metadata_group::Column::Identifier.eq(&input.unique.identifier))
+        .filter(metadata_group::Column::Lot.eq(input.unique.lot))
+        .filter(metadata_group::Column::Source.eq(input.unique.source))
+        .one(&ss.db)
+        .await?
+        .map(|m| StringIdObject { id: m.id })
+    {
+        Some(m) => Ok(m),
+        None => {
+            let new_group = metadata_group::ActiveModel {
+                parts: ActiveValue::Set(0),
+                title: ActiveValue::Set(input.name),
+                lot: ActiveValue::Set(input.unique.lot),
+                is_partial: ActiveValue::Set(Some(true)),
+                source: ActiveValue::Set(input.unique.source),
+                identifier: ActiveValue::Set(input.unique.identifier.clone()),
+                ..Default::default()
+            };
+            let new_group = new_group.insert(&ss.db).await?;
+            Ok(StringIdObject { id: new_group.id })
+        }
+    }
+}
+
+pub async fn commit_person(
+    input: CommitPersonInput,
+    db: &DatabaseConnection,
+) -> Result<StringIdObject> {
+    match Person::find()
+        .filter(person::Column::Source.eq(input.source))
+        .filter(person::Column::Identifier.eq(&input.identifier))
+        .apply_if(input.source_specifics.clone(), |query, v| {
+            query.filter(person::Column::SourceSpecifics.eq(v))
+        })
+        .one(db)
+        .await?
+        .map(|p| StringIdObject { id: p.id })
+    {
+        Some(p) => Ok(p),
+        None => {
+            let person = person::ActiveModel {
+                name: ActiveValue::Set(input.name),
+                source: ActiveValue::Set(input.source),
+                is_partial: ActiveValue::Set(Some(true)),
+                identifier: ActiveValue::Set(input.identifier),
+                source_specifics: ActiveValue::Set(input.source_specifics),
+                ..Default::default()
+            };
+            let person = person.insert(db).await?;
+            Ok(StringIdObject { id: person.id })
+        }
+    }
 }
 
 pub async fn commit_metadata(
     input: CommitMediaInput,
     ss: &Arc<SupportingService>,
-) -> Result<metadata::Model> {
-    let Some(m) = Metadata::find()
-        .filter(metadata::Column::Lot.eq(input.lot))
-        .filter(metadata::Column::Source.eq(input.source))
-        .filter(metadata::Column::Identifier.eq(input.identifier.clone()))
+) -> Result<StringIdObject> {
+    match Metadata::find()
+        .filter(metadata::Column::Identifier.eq(&input.unique.identifier))
+        .filter(metadata::Column::Lot.eq(input.unique.lot))
+        .filter(metadata::Column::Source.eq(input.unique.source))
         .one(&ss.db)
         .await?
-    else {
-        let details = details_from_provider(input.lot, input.source, &input.identifier, ss).await?;
-        let media = commit_metadata_internal(details, None, ss).await?;
-        return Ok(media);
-    };
-    if input.force_update.unwrap_or_default() {
-        ryot_log!(debug, "Forcing update of metadata with id {}", &m.id);
-        update_metadata_and_notify_users(&m.id, true, ss).await?;
+        .map(|m| StringIdObject { id: m.id })
+    {
+        Some(m) => Ok(m),
+        None => {
+            let new_metadata = metadata::ActiveModel {
+                title: ActiveValue::Set(input.name),
+                lot: ActiveValue::Set(input.unique.lot),
+                is_partial: ActiveValue::Set(Some(true)),
+                source: ActiveValue::Set(input.unique.source),
+                identifier: ActiveValue::Set(input.unique.identifier.clone()),
+                ..Default::default()
+            };
+            let new_metadata = new_metadata.insert(&ss.db).await?.id;
+            Ok(StringIdObject { id: new_metadata })
+        }
     }
-    Ok(m)
 }
 
 pub async fn deploy_update_metadata_job(
     metadata_id: &String,
-    force_update: bool,
     ss: &Arc<SupportingService>,
 ) -> Result<bool> {
-    ss.perform_application_job(ApplicationJob::UpdateMetadata(
+    ss.perform_application_job(ApplicationJob::Mp(MpApplicationJob::UpdateMetadata(
         metadata_id.to_owned(),
-        force_update,
-    ))
+    )))
     .await?;
     Ok(true)
 }
@@ -894,37 +840,42 @@ pub async fn deploy_background_job(
                 .await
                 .unwrap();
             for metadata_id in many_metadata {
-                deploy_update_metadata_job(&metadata_id, true, ss).await?;
+                deploy_update_metadata_job(&metadata_id, ss).await?;
             }
         }
         BackgroundJob::UpdateAllExercises => {
-            ss.perform_application_job(ApplicationJob::UpdateExerciseLibrary)
+            ss.perform_application_job(ApplicationJob::Mp(MpApplicationJob::UpdateExerciseLibrary))
                 .await?;
         }
         BackgroundJob::RecalculateCalendarEvents => {
-            ss.perform_application_job(ApplicationJob::RecalculateCalendarEvents)
-                .await?;
+            ss.perform_application_job(ApplicationJob::Mp(
+                MpApplicationJob::RecalculateCalendarEvents,
+            ))
+            .await?;
         }
         BackgroundJob::PerformBackgroundTasks => {
-            ss.perform_application_job(ApplicationJob::PerformBackgroundTasks)
-                .await?;
+            ss.perform_application_job(ApplicationJob::Mp(
+                MpApplicationJob::PerformBackgroundTasks,
+            ))
+            .await?;
         }
         BackgroundJob::SyncIntegrationsData => {
-            ss.perform_core_application_job(CoreApplicationJob::SyncIntegrationsData(
-                user_id.to_owned(),
+            ss.perform_application_job(ApplicationJob::Hp(
+                HpApplicationJob::SyncUserIntegrationsData(user_id.to_owned()),
             ))
             .await?;
         }
         BackgroundJob::CalculateUserActivitiesAndSummary => {
-            ss.perform_application_job(ApplicationJob::RecalculateUserActivitiesAndSummary(
-                user_id.to_owned(),
-                true,
+            ss.perform_application_job(ApplicationJob::Hp(
+                HpApplicationJob::RecalculateUserActivitiesAndSummary(user_id.to_owned(), true),
             ))
             .await?;
         }
         BackgroundJob::ReviseUserWorkouts => {
-            ss.perform_application_job(ApplicationJob::ReviseUserWorkouts(user_id.to_owned()))
-                .await?;
+            ss.perform_application_job(ApplicationJob::Mp(MpApplicationJob::ReviseUserWorkouts(
+                user_id.to_owned(),
+            )))
+            .await?;
         }
     };
     Ok(true)
@@ -1038,13 +989,15 @@ pub async fn post_review(
         let user = user_by_id(&insert.user_id.unwrap(), ss).await?;
         // DEV: Do not send notification if updating a review
         if input.review_id.is_none() {
-            ss.perform_core_application_job(CoreApplicationJob::ReviewPosted(ReviewPostedEvent {
-                obj_title,
-                entity_lot,
-                obj_id: id,
-                username: user.name,
-                review_id: insert.id.clone().unwrap(),
-            }))
+            ss.perform_application_job(ApplicationJob::Hp(HpApplicationJob::ReviewPosted(
+                ReviewPostedEvent {
+                    obj_title,
+                    entity_lot,
+                    obj_id: id,
+                    username: user.name,
+                    review_id: insert.id.clone().unwrap(),
+                },
+            )))
             .await?;
         }
     }
@@ -1054,42 +1007,6 @@ pub async fn post_review(
     })
 }
 
-pub async fn commit_metadata_group_internal(
-    identifier: &String,
-    lot: MediaLot,
-    source: MediaSource,
-    ss: &Arc<SupportingService>,
-) -> Result<(String, Vec<PartialMetadataWithoutId>)> {
-    let existing_group = MetadataGroup::find()
-        .filter(metadata_group::Column::Identifier.eq(identifier))
-        .filter(metadata_group::Column::Lot.eq(lot))
-        .filter(metadata_group::Column::Source.eq(source))
-        .one(&ss.db)
-        .await?;
-    let provider = get_metadata_provider(lot, source, ss).await?;
-    let (group_details, associated_items) = provider.metadata_group_details(identifier).await?;
-    let group_id = match existing_group {
-        Some(eg) => {
-            let mut eg: metadata_group::ActiveModel = eg.into();
-            eg.parts = ActiveValue::Set(group_details.parts);
-            eg.source_url = ActiveValue::Set(group_details.source_url);
-            eg.images = ActiveValue::Set(group_details.images.filter(|i| !i.is_empty()));
-            let eg = eg.update(&ss.db).await?;
-            eg.id
-        }
-        None => {
-            let mut group_details = group_details.clone();
-            group_details.images = group_details.images.filter(|i| !i.is_empty());
-            let mut db_group: metadata_group::ActiveModel =
-                group_details.into_model("".to_string(), None).into();
-            db_group.id = ActiveValue::NotSet;
-            let new_group = db_group.insert(&ss.db).await?;
-            new_group.id
-        }
-    };
-    Ok((group_id, associated_items))
-}
-
 async fn seen_history(
     user_id: &String,
     metadata_id: &String,
@@ -1192,8 +1109,10 @@ pub async fn deploy_after_handle_media_seen_tasks(
     seen: seen::Model,
     ss: &Arc<SupportingService>,
 ) -> Result<()> {
-    ss.perform_application_job(ApplicationJob::HandleAfterMediaSeenTasks(seen))
-        .await
+    ss.perform_application_job(ApplicationJob::Lp(
+        LpApplicationJob::HandleAfterMediaSeenTasks(seen),
+    ))
+    .await
 }
 
 pub async fn handle_after_media_seen_tasks(
@@ -1215,7 +1134,6 @@ pub async fn handle_after_media_seen_tasks(
     };
     let remove_entity_from_collection = |collection_name: &str| {
         remove_entity_from_collection(
-            &ss.db,
             &seen.user_id,
             ChangeCollectionToEntityInput {
                 creator_user_id: seen.user_id.clone(),
@@ -1224,6 +1142,7 @@ pub async fn handle_after_media_seen_tasks(
                 entity_lot: EntityLot::Metadata,
                 ..Default::default()
             },
+            ss,
         )
     };
     remove_entity_from_collection(&DefaultCollection::Watchlist.to_string())
@@ -1570,9 +1489,12 @@ pub async fn progress_update(
             .await?;
     }
     if seen.state == SeenState::Completed {
-        ss.perform_application_job(ApplicationJob::HandleOnSeenComplete(seen.id.clone()))
-            .await?;
+        ss.perform_application_job(ApplicationJob::Lp(LpApplicationJob::HandleOnSeenComplete(
+            seen.id.clone(),
+        )))
+        .await?;
     }
+    expire_user_collections_list_cache(user_id, ss).await?;
     deploy_after_handle_media_seen_tasks(seen, ss).await?;
     Ok(ProgressUpdateResultUnion::Ok(StringIdObject { id }))
 }
@@ -1790,7 +1712,7 @@ pub async fn create_or_update_user_workout(
     if input.exercises.is_empty() {
         return Err(Error::new("This workout has no associated exercises"));
     }
-    let mut first_set_of_exercise_confirmed_at = input
+    let mut first_set_confirmed_at = input
         .exercises
         .first()
         .unwrap()
@@ -1821,21 +1743,21 @@ pub async fn create_or_update_user_workout(
         let history_item = UserToExerciseHistoryExtraInformation {
             best_set: None,
             idx: exercise_idx,
-            workout_id: new_workout_id.clone(),
             workout_end_on: end_time,
+            workout_id: new_workout_id.clone(),
         };
         let asc = match association {
             Some(e) => e,
             None => {
+                let timestamp = first_set_confirmed_at.unwrap_or(end_time);
                 let user_to_ex = user_to_entity::ActiveModel {
+                    created_on: ActiveValue::Set(timestamp),
                     user_id: ActiveValue::Set(user_id.clone()),
+                    last_updated_on: ActiveValue::Set(timestamp),
                     exercise_id: ActiveValue::Set(Some(ex.exercise_id.clone())),
                     exercise_extra_information: ActiveValue::Set(Some(
                         UserToExerciseExtraInformation::default(),
                     )),
-                    created_on: ActiveValue::Set(
-                        first_set_of_exercise_confirmed_at.unwrap_or(end_time),
-                    ),
                     ..Default::default()
                 };
                 user_to_ex.insert(&ss.db).await.unwrap()
@@ -1849,7 +1771,7 @@ pub async fn create_or_update_user_workout(
             ActiveValue::Set(Some(extra_info.history.len().try_into().unwrap()));
         to_update.exercise_extra_information = ActiveValue::Set(Some(extra_info));
         to_update.last_updated_on =
-            ActiveValue::Set(first_set_of_exercise_confirmed_at.unwrap_or(last_updated_on));
+            ActiveValue::Set(first_set_confirmed_at.unwrap_or(last_updated_on));
         let association = to_update.update(&ss.db).await?;
         totals.rest_time = ex
             .sets
@@ -1859,7 +1781,7 @@ pub async fn create_or_update_user_workout(
         ex.sets
             .sort_unstable_by_key(|s| s.confirmed_at.unwrap_or_default());
         for set in ex.sets.iter_mut() {
-            first_set_of_exercise_confirmed_at = set.confirmed_at;
+            first_set_confirmed_at = set.confirmed_at;
             clean_values(set, &db_ex.lot);
             if let Some(r) = set.statistic.reps {
                 totals.reps += r;
@@ -2045,12 +1967,12 @@ async fn create_collection_and_add_entity_to_it(
     entity_lot: EntityLot,
 ) -> Result<()> {
     create_or_update_collection(
-        &ss.db,
         user_id,
         CreateOrUpdateCollectionInput {
             name: collection_name.clone(),
             ..Default::default()
         },
+        ss,
     )
     .await?;
     add_entity_to_collection(
@@ -2110,6 +2032,7 @@ pub async fn create_custom_exercise(
 
 pub async fn process_import<F>(
     user_id: &String,
+    run_updates: bool,
     respect_cache: bool,
     import: ImportResult,
     ss: &Arc<SupportingService>,
@@ -2121,7 +2044,7 @@ where
     let preferences = user_by_id(user_id, ss).await?.preferences;
     let mut import = import;
     // DEV: We need to sort the exercises to make sure they are created before the workouts
-    // DEV: because the workouts depend on the exercises.
+    // because the workouts depend on the exercises.
     import.completed.sort_by_key(|i| match i {
         ImportCompletedItem::Exercise(_) => 0,
         _ => 1,
@@ -2131,6 +2054,10 @@ where
 
     let mut need_to_schedule_user_for_workout_revision = false;
 
+    let mut metadata_to_update = HashSet::new();
+    let mut metadata_groups_to_update = HashSet::new();
+    let mut people_to_update = HashSet::new();
+
     for (idx, item) in import.completed.into_iter().enumerate() {
         ryot_log!(
             debug,
@@ -2145,9 +2072,15 @@ where
                 create_custom_exercise(user_id, exercise, ss).await?;
             }
             ImportCompletedItem::Collection(col_details) => {
-                create_or_update_collection(&ss.db, user_id, col_details).await?;
+                create_or_update_collection(user_id, col_details, ss).await?;
             }
             ImportCompletedItem::Metadata(metadata) => {
+                if metadata.seen_history.is_empty()
+                    && metadata.reviews.is_empty()
+                    && metadata.collections.is_empty()
+                {
+                    continue;
+                }
                 let source_id = if metadata.source_id.is_empty() {
                     metadata.identifier.clone()
                 } else {
@@ -2157,10 +2090,12 @@ where
                 let identifier = metadata.identifier.clone();
                 let db_metadata = match commit_metadata(
                     CommitMediaInput {
-                        identifier,
-                        lot: metadata.lot,
-                        force_update: None,
-                        source: metadata.source,
+                        name: metadata.source_id.clone(),
+                        unique: UniqueMediaIdentifier {
+                            identifier,
+                            lot: metadata.lot,
+                            source: metadata.source,
+                        },
                     },
                     ss,
                 )
@@ -2168,7 +2103,6 @@ where
                 {
                     Ok(r) => r,
                     Err(e) => {
-                        ryot_log!(error, "{e:?}");
                         import.failed.push(ImportFailedItem {
                             lot: Some(metadata.lot),
                             step: ImportFailStep::MediaDetailsFromProvider,
@@ -2178,6 +2112,7 @@ where
                         continue;
                     }
                 };
+                metadata_to_update.insert(db_metadata.id.clone());
                 for seen in metadata.seen_history.iter() {
                     let progress = if seen.progress.is_some() {
                         seen.progress
@@ -2242,17 +2177,24 @@ where
                 }
             }
             ImportCompletedItem::MetadataGroup(metadata_group) => {
-                let metadata_group_id = match commit_metadata_group_internal(
-                    &metadata_group.identifier,
-                    metadata_group.lot,
-                    metadata_group.source,
+                if metadata_group.reviews.is_empty() && metadata_group.collections.is_empty() {
+                    continue;
+                }
+                let metadata_group_id = match commit_metadata_group(
+                    CommitMediaInput {
+                        name: metadata_group.title.clone(),
+                        unique: UniqueMediaIdentifier {
+                            lot: metadata_group.lot,
+                            source: metadata_group.source,
+                            identifier: metadata_group.identifier,
+                        },
+                    },
                     ss,
                 )
                 .await
                 {
-                    Ok(r) => r.0,
+                    Ok(r) => r.id,
                     Err(e) => {
-                        ryot_log!(error, "{e:?}");
                         import.failed.push(ImportFailedItem {
                             lot: Some(metadata_group.lot),
                             step: ImportFailStep::MediaDetailsFromProvider,
@@ -2262,6 +2204,7 @@ where
                         continue;
                     }
                 };
+                metadata_groups_to_update.insert(metadata_group_id.clone());
                 for review in metadata_group.reviews.iter() {
                     if let Some(input) = convert_review_into_input(
                         review,
@@ -2291,7 +2234,10 @@ where
                 }
             }
             ImportCompletedItem::Person(person) => {
-                let db_person = commit_person(
+                if person.reviews.is_empty() && person.collections.is_empty() {
+                    continue;
+                }
+                let db_person = match commit_person(
                     CommitPersonInput {
                         identifier: person.identifier.clone(),
                         name: person.name.clone(),
@@ -2300,7 +2246,20 @@ where
                     },
                     &ss.db,
                 )
-                .await?;
+                .await
+                {
+                    Ok(p) => p,
+                    Err(e) => {
+                        import.failed.push(ImportFailedItem {
+                            lot: None,
+                            error: Some(e.message),
+                            identifier: person.identifier.clone(),
+                            step: ImportFailStep::MediaDetailsFromProvider,
+                        });
+                        continue;
+                    }
+                };
+                people_to_update.insert(db_person.id.clone());
                 for review in person.reviews.iter() {
                     if let Some(input) = convert_review_into_input(
                         review,
@@ -2386,6 +2345,24 @@ where
         schedule_user_for_workout_revision(user_id, ss).await?;
     }
 
+    if run_updates {
+        for metadata_id in metadata_to_update {
+            deploy_update_metadata_job(&metadata_id, ss).await?;
+        }
+        for metadata_group_id in metadata_groups_to_update {
+            ss.perform_application_job(ApplicationJob::Mp(MpApplicationJob::UpdateMetadataGroup(
+                metadata_group_id,
+            )))
+            .await?;
+        }
+        for person_id in people_to_update {
+            ss.perform_application_job(ApplicationJob::Mp(MpApplicationJob::UpdatePerson(
+                person_id,
+            )))
+            .await?;
+        }
+    }
+
     let details = ImportResultResponse {
         failed_items: import.failed,
         import: ImportDetails { total },
@@ -2496,7 +2473,133 @@ pub async fn add_entity_to_collection(
         created
     };
     mark_entity_as_recently_consumed(user_id, &input.entity_id, input.entity_lot, ss).await?;
-    ss.perform_application_job(ApplicationJob::HandleEntityAddedToCollectionEvent(resp.id))
-        .await?;
+    ss.perform_application_job(ApplicationJob::Lp(
+        LpApplicationJob::HandleEntityAddedToCollectionEvent(resp.id),
+    ))
+    .await?;
+    expire_user_collections_list_cache(user_id, ss).await?;
     Ok(true)
 }
+
+pub async fn get_identifier_from_book_isbn(
+    isbn: &str,
+    google_books_service: &GoogleBooksService,
+    open_library_service: &OpenlibraryService,
+) -> Option<(String, MediaSource)> {
+    let mut identifier = None;
+    let mut source = MediaSource::GoogleBooks;
+    if let Some(id) = google_books_service.id_from_isbn(isbn).await {
+        identifier = Some(id);
+    } else if let Some(id) = open_library_service.id_from_isbn(isbn).await {
+        identifier = Some(id);
+        source = MediaSource::Openlibrary;
+    }
+    identifier.map(|id| (id, source))
+}
+
+pub async fn expire_user_collections_list_cache(
+    user_id: &String,
+    ss: &Arc<SupportingService>,
+) -> Result<()> {
+    let cache_key = ApplicationCacheKey::UserCollectionsList(UserLevelCacheKey {
+        input: (),
+        user_id: user_id.to_owned(),
+    });
+    ss.cache_service.expire_key(cache_key).await?;
+    Ok(())
+}
+
+pub async fn create_or_update_collection(
+    user_id: &String,
+    input: CreateOrUpdateCollectionInput,
+    ss: &Arc<SupportingService>,
+) -> Result<StringIdObject> {
+    let txn = ss.db.begin().await?;
+    let meta = Collection::find()
+        .filter(collection::Column::Name.eq(input.name.clone()))
+        .filter(collection::Column::UserId.eq(user_id))
+        .one(&txn)
+        .await
+        .unwrap();
+    let mut new_name = input.name.clone();
+    let created = match meta {
+        Some(m) if input.update_id.is_none() => m.id,
+        _ => {
+            let col = collection::ActiveModel {
+                id: match input.update_id {
+                    Some(i) => {
+                        let already = Collection::find_by_id(i.clone())
+                            .one(&txn)
+                            .await
+                            .unwrap()
+                            .unwrap();
+                        if DefaultCollection::iter()
+                            .map(|s| s.to_string())
+                            .contains(&already.name)
+                        {
+                            new_name = already.name;
+                        }
+                        ActiveValue::Unchanged(i.clone())
+                    }
+                    None => ActiveValue::NotSet,
+                },
+                last_updated_on: ActiveValue::Set(Utc::now()),
+                name: ActiveValue::Set(new_name),
+                user_id: ActiveValue::Set(user_id.to_owned()),
+                description: ActiveValue::Set(input.description),
+                information_template: ActiveValue::Set(input.information_template),
+                ..Default::default()
+            };
+            let inserted = col
+                .save(&txn)
+                .await
+                .map_err(|_| Error::new("There was an error creating the collection".to_owned()))?;
+            let id = inserted.id.unwrap();
+            let mut collaborators = vec![user_id.to_owned()];
+            if let Some(input_collaborators) = input.collaborators {
+                collaborators.extend(input_collaborators);
+            }
+            let inserts = collaborators
+                .into_iter()
+                .map(|c| user_to_entity::ActiveModel {
+                    user_id: ActiveValue::Set(c),
+                    collection_id: ActiveValue::Set(Some(id.clone())),
+                    ..Default::default()
+                });
+            UserToEntity::insert_many(inserts)
+                .on_conflict(OnConflict::new().do_nothing().to_owned())
+                .exec_without_returning(&txn)
+                .await?;
+            id
+        }
+    };
+    txn.commit().await?;
+    expire_user_collections_list_cache(user_id, ss).await?;
+    Ok(StringIdObject { id: created })
+}
+
+pub async fn remove_entity_from_collection(
+    user_id: &String,
+    input: ChangeCollectionToEntityInput,
+    ss: &Arc<SupportingService>,
+) -> Result<StringIdObject> {
+    let collect = Collection::find()
+        .left_join(UserToEntity)
+        .filter(collection::Column::Name.eq(input.collection_name))
+        .filter(user_to_entity::Column::UserId.eq(input.creator_user_id))
+        .one(&ss.db)
+        .await
+        .unwrap()
+        .unwrap();
+    let column = get_cte_column_from_lot(input.entity_lot);
+    CollectionToEntity::delete_many()
+        .filter(collection_to_entity::Column::CollectionId.eq(collect.id.clone()))
+        .filter(column.eq(input.entity_id.clone()))
+        .exec(&ss.db)
+        .await?;
+    if input.entity_lot != EntityLot::Workout && input.entity_lot != EntityLot::WorkoutTemplate {
+        associate_user_with_entity(&ss.db, user_id, &input.entity_id, input.entity_lot).await?;
+    }
+    expire_user_collections_list_cache(user_id, ss).await?;
+    Ok(StringIdObject { id: collect.id })
+}
diff --git a/crates/utils/specific/Cargo.toml b/crates/utils/specific/Cargo.toml
new file mode 100644
index 0000000000..1deb8b2ffd
--- /dev/null
+++ b/crates/utils/specific/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "specific-utils"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+anyhow = { workspace = true }
+database-models = { path = "../../models/database" }
+dependent-utils = { path = "../dependent" }
+enum-models = { path = "../../models/enum" }
+sea-orm = { workspace = true }
+supporting-service = { path = "../../services/supporting" }
+tokio = { workspace = true }
diff --git a/crates/utils/specific/src/lib.rs b/crates/utils/specific/src/lib.rs
new file mode 100644
index 0000000000..25dd3afd75
--- /dev/null
+++ b/crates/utils/specific/src/lib.rs
@@ -0,0 +1,38 @@
+use anyhow::Result;
+use database_models::{metadata, prelude::Metadata};
+use dependent_utils::deploy_update_metadata_job;
+use enum_models::{MediaLot, MediaSource};
+use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
+use std::sync::Arc;
+use supporting_service::SupportingService;
+use tokio::time::{sleep, Duration};
+
+pub mod audiobookshelf {
+    use super::*;
+
+    pub async fn get_updated_metadata(
+        identifier: &String,
+        ss: &Arc<SupportingService>,
+    ) -> Result<metadata::Model> {
+        async fn get_metadata(
+            identifier: &String,
+            ss: &Arc<SupportingService>,
+        ) -> Result<metadata::Model> {
+            let m = Metadata::find()
+                .filter(metadata::Column::Identifier.eq(identifier))
+                .filter(metadata::Column::Lot.eq(MediaLot::Podcast))
+                .filter(metadata::Column::Source.eq(MediaSource::Itunes))
+                .one(&ss.db)
+                .await?
+                .unwrap();
+            Ok(m)
+        }
+
+        let already = get_metadata(identifier, ss).await?;
+        if already.podcast_specifics.is_none() {
+            deploy_update_metadata_job(&already.id, ss).await.unwrap();
+            sleep(Duration::from_secs(3)).await;
+        }
+        get_metadata(identifier, ss).await
+    }
+}
diff --git a/docs/content/importing.md b/docs/content/importing.md
index beb5659574..7186f107b9 100644
--- a/docs/content/importing.md
+++ b/docs/content/importing.md
@@ -147,7 +147,7 @@ ratings, history and progress.
 ## Anilist
 
 Manga and anime can be imported from [Anilist](https://anilist.co) along with ratings,
-history and progress.
+history, favorites and custom lists.
 
 ### Steps
 
diff --git a/docs/includes/backend-config-schema.yaml b/docs/includes/backend-config-schema.yaml
index 5df653dcd7..9d69dabfb5 100644
--- a/docs/includes/backend-config-schema.yaml
+++ b/docs/includes/backend-config-schema.yaml
@@ -146,13 +146,6 @@ podcasts:
     # @envvar PODCASTS_LISTENNOTES_API_TOKEN
     api_token: ""
 
-# Settings related to scheduler.
-scheduler:
-  # The number of jobs to process every 5 seconds when updating metadata in
-  # the background.
-  # @envvar SCHEDULER_RATE_LIMIT_NUM
-  rate_limit_num: 5
-
 # Settings related to server.
 server:
   # An access token that can be used for admin operations.
diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts
index 0ef118dd0c..573f5f9e07 100644
--- a/libs/generated/src/graphql/backend/graphql.ts
+++ b/libs/generated/src/graphql/backend/graphql.ts
@@ -215,10 +215,8 @@ export type CollectionItem = {
 };
 
 export type CommitMediaInput = {
-  forceUpdate?: InputMaybe<Scalars['Boolean']['input']>;
-  identifier: Scalars['String']['input'];
-  lot: MediaLot;
-  source: MediaSource;
+  name: Scalars['String']['input'];
+  unique: UniqueMediaIdentifier;
 };
 
 export type CommitPersonInput = {
@@ -2174,6 +2172,12 @@ export type StringIdObject = {
   id: Scalars['String']['output'];
 };
 
+export type UniqueMediaIdentifier = {
+  identifier: Scalars['String']['input'];
+  lot: MediaLot;
+  source: MediaSource;
+};
+
 export type UpdateComplexJsonInput = {
   /** Dot delimited path to the property that needs to be changed. */
   property: Scalars['String']['input'];
diff --git a/libs/generated/src/graphql/backend/types.generated.ts b/libs/generated/src/graphql/backend/types.generated.ts
index d87407fb5b..239d246a11 100644
--- a/libs/generated/src/graphql/backend/types.generated.ts
+++ b/libs/generated/src/graphql/backend/types.generated.ts
@@ -200,10 +200,8 @@ export type CollectionItem = {
 };
 
 export type CommitMediaInput = {
-  forceUpdate?: InputMaybe<Scalars['Boolean']['input']>;
-  identifier: Scalars['String']['input'];
-  lot: MediaLot;
-  source: MediaSource;
+  name: Scalars['String']['input'];
+  unique: UniqueMediaIdentifier;
 };
 
 export type CommitPersonInput = {
@@ -2246,6 +2244,12 @@ export type StringIdObject = {
   id: Scalars['String']['output'];
 };
 
+export type UniqueMediaIdentifier = {
+  identifier: Scalars['String']['input'];
+  lot: MediaLot;
+  source: MediaSource;
+};
+
 export type UpdateComplexJsonInput = {
   /** Dot delimited path to the property that needs to be changed. */
   property: Scalars['String']['input'];


Test patch: