Repository: IgnisDa/ryot (language: rust)

Issue:
Add webhook endpoint that supports generic json
If I wanted to sort out a goodreads export and supplement the data to make it import properly (had 280 fail with 140 success) upon import.

So i was thinking making a json file to follow the format of an export for books.

Is there something I can call (locally in the container, or externally), to import a json file?

Or is there a better way to do it?   I've never used rust before, but it's it's relatively easier to pick up I will.


Discussion / hints:
https://docs.ryot.io/importing#generic-json
Yeah that says about the format of the json, which I mentioned I was going to use.

But I'd rather not manually import json files.

How can I initiate the import via script?
You need to create just one json file and import everything.

You definitely can use SQL but the application uses triggers etc which won't run so I suspect it would cause you a lot of pain.

What's wrong with importing json?
I was expecting to do it more than once, and maybe ongoing.


No it supports an array for everything it can import, so you can just upload one json file.
yes but I'm intending to run this daily with imports.  Which is why I don't want to manually initiate it.
What's your use case? Why do you need to run imports everyday?
Similar to someone else, sites not currently supported, so I can handle those.

Ie I might use a calibre extract which I've seen you're going to incorporate due to number of users - which is fine.  I'm happy to make something to do that.

And if i can take a feed from a plex scrobbler to trakt (or tautulli) then I can run that.
Just a couple of examples i can think of off the top of my head. 

My initial will be a single manual upload from goodreads

I think this would be better served as a webhook.
What's your thinking here?

Ryot fire off a hook to pull in data?

So we'd need something to receive that and provide the data.

I am thinking of an endpoint that any external service can call to update progress. It will support the same data format as generic json.
Sounds good. 
Would you be willing to make a PR for it?
To build it?  I'm not sure I would be able to. 
No issues. thanks for the feature request.

Fix patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index fedc440753..697b7e7d75 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -161,6 +161,12 @@ jobs:
         with:
           key: ${{ matrix.platform.target }}-${{ steps.build.outputs.profile }}
           save-if: ${{ github.event_name != 'pull_request' }}
+      - uses: actions/cache@v4
+        with:
+          path: target
+          key: cargo-build-${{ runner.os }}-${{ matrix.platform.target }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}
+          restore-keys: |
+            cargo-build-${{ runner.os }}-${{ matrix.platform.target }}-
       - name: Install cross
         if: ${{ matrix.platform.command == 'cross' }}
         uses: taiki-e/cache-cargo-install-action@v2
diff --git a/Cargo.lock b/Cargo.lock
index 393737b15a..f49c3fe399 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -95,9 +95,9 @@ dependencies = [
 
 [[package]]
 name = "anstream"
-version = "0.6.14"
+version = "0.6.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
+checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
 dependencies = [
  "anstyle",
  "anstyle-parse",
@@ -110,33 +110,33 @@ dependencies = [
 
 [[package]]
 name = "anstyle"
-version = "1.0.7"
+version = "1.0.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
+checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
 
 [[package]]
 name = "anstyle-parse"
-version = "0.2.4"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
+checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
 dependencies = [
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle-query"
-version = "1.1.0"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
+checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
 dependencies = [
  "windows-sys 0.52.0",
 ]
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.3"
+version = "3.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
+checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
 dependencies = [
  "anstyle",
  "windows-sys 0.52.0",
@@ -144,9 +144,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.82"
+version = "1.0.89"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
+checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
 
 [[package]]
 name = "apalis"
@@ -163,7 +163,7 @@ dependencies = [
  "serde",
  "thiserror",
  "tokio",
- "tower",
+ "tower 0.4.13",
  "tracing",
  "tracing-futures",
 ]
@@ -181,7 +181,7 @@ dependencies = [
  "serde",
  "serde_json",
  "thiserror",
- "tower",
+ "tower 0.4.13",
  "ulid",
 ]
 
@@ -196,7 +196,7 @@ dependencies = [
  "chrono",
  "cron",
  "futures",
- "tower",
+ "tower 0.4.13",
 ]
 
 [[package]]
@@ -246,7 +246,7 @@ dependencies = [
  "config",
  "file-storage-service",
  "jwt-service",
- "reqwest 0.12.7",
+ "reqwest 0.12.8",
 ]
 
 [[package]]
@@ -269,9 +269,9 @@ dependencies = [
 
 [[package]]
 name = "arrayvec"
-version = "0.7.4"
+version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
 
 [[package]]
 name = "ascii"
@@ -331,9 +331,9 @@ dependencies = [
 
 [[package]]
 name = "async-graphql"
-version = "7.0.9"
+version = "7.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d37c3e9ba322eb00e9e5e997d58f08e8b6de037325b9367ac59bca8e3cd46af"
+checksum = "0ba6d24703c5adc5ba9116901b92ee4e4c0643c01a56c4fd303f3818638d7449"
 dependencies = [
  "async-graphql-derive",
  "async-graphql-parser",
@@ -370,9 +370,9 @@ dependencies = [
 
 [[package]]
 name = "async-graphql-axum"
-version = "7.0.9"
+version = "7.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "329afc4fc7359f112b7593bc930b788544cedbc97c4fbdf1db21e58704b4b5d0"
+checksum = "e9aa80e171205c6d562057fd5a49167c8fbe61f7db2bed6540f6d4f2234d7ff2"
 dependencies = [
  "async-graphql",
  "async-trait",
@@ -388,9 +388,9 @@ dependencies = [
 
 [[package]]
 name = "async-graphql-derive"
-version = "7.0.9"
+version = "7.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1141703c11c6ad4fa9b3b0e1e476dea01dbd18a44db00f949b804afaab2f344"
+checksum = "a94c2d176893486bd37cd1b6defadd999f7357bf5804e92f510c08bcf16c538f"
 dependencies = [
  "Inflector",
  "async-graphql-parser",
@@ -405,9 +405,9 @@ dependencies = [
 
 [[package]]
 name = "async-graphql-parser"
-version = "7.0.9"
+version = "7.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f66edcce4c38c18f7eb181fdf561c3d3aa2d644ce7358fc7a928c00a4ffef17"
+checksum = "79272bdbf26af97866e149f05b2b546edb5c00e51b5f916289931ed233e208ad"
 dependencies = [
  "async-graphql-value",
  "pest",
@@ -417,9 +417,9 @@ dependencies = [
 
 [[package]]
 name = "async-graphql-value"
-version = "7.0.9"
+version = "7.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b0206011cad065420c27988f17dd7fe201a0e056b20c262209b7bffcd6fa176"
+checksum = "ef5ec94176a12a8cbe985cd73f2e54dc9c702c88c766bdef12f1f3a67cedbee1"
 dependencies = [
  "bytes",
  "indexmap 2.5.0",
@@ -471,9 +471,9 @@ dependencies = [
 
 [[package]]
 name = "async-trait"
-version = "0.1.81"
+version = "0.1.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
+checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -503,9 +503,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
 
 [[package]]
 name = "aws-credential-types"
-version = "1.2.0"
+version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e16838e6c9e12125face1c1eff1343c75e3ff540de98ff7ebd61874a89bcfeb9"
+checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-runtime-api",
@@ -515,15 +515,16 @@ dependencies = [
 
 [[package]]
 name = "aws-runtime"
-version = "1.3.1"
+version = "1.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87c5f920ffd1e0526ec9e70e50bf444db50b204395a0fa7016bbf9e31ea1698f"
+checksum = "a10d5c055aa540164d9561a0e2e74ad30f0dcf7393c3a92f6733ddf9c5762468"
 dependencies = [
  "aws-credential-types",
  "aws-sigv4",
  "aws-smithy-async",
  "aws-smithy-eventstream",
  "aws-smithy-http",
+ "aws-smithy-runtime",
  "aws-smithy-runtime-api",
  "aws-smithy-types",
  "aws-types",
@@ -531,6 +532,7 @@ dependencies = [
  "fastrand",
  "http 0.2.12",
  "http-body 0.4.6",
+ "once_cell",
  "percent-encoding",
  "pin-project-lite",
  "tracing",
@@ -539,9 +541,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-s3"
-version = "1.42.0"
+version = "1.54.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "558bbcec8db82a1a8af1610afcb3b10d00652d25ad366a0558eecdff2400a1d1"
+checksum = "e2f2a62020f3e06f9b352b2a23547f6e1d110b6bf1e18a6b588ae36114eaf6e2"
 dependencies = [
  "ahash 0.8.11",
  "aws-credential-types",
@@ -574,9 +576,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sigv4"
-version = "1.2.3"
+version = "1.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5df1b0fa6be58efe9d4ccc257df0a53b89cd8909e86591a13ca54817c87517be"
+checksum = "cc8db6904450bafe7473c6ca9123f88cc11089e41a025408f992db4e22d3be68"
 dependencies = [
  "aws-credential-types",
  "aws-smithy-eventstream",
@@ -614,9 +616,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-checksums"
-version = "0.60.11"
+version = "0.60.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48c4134cf3adaeacff34d588dbe814200357b0c466d730cf1c0d8054384a2de4"
+checksum = "598b1689d001c4d4dc3cb386adb07d37786783aee3ac4b324bcadac116bf3d23"
 dependencies = [
  "aws-smithy-http",
  "aws-smithy-types",
@@ -635,9 +637,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-eventstream"
-version = "0.60.4"
+version = "0.60.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6363078f927f612b970edf9d1903ef5cef9a64d1e8423525ebb1f0a1633c858"
+checksum = "cef7d0a272725f87e51ba2bf89f8c21e4df61b9e49ae1ac367a6d69916ef7c90"
 dependencies = [
  "aws-smithy-types",
  "bytes",
@@ -646,9 +648,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-http"
-version = "0.60.9"
+version = "0.60.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9cd0ae3d97daa0a2bf377a4d8e8e1362cae590c4a1aad0d40058ebca18eb91e"
+checksum = "5c8bc3e8fdc6b8d07d976e301c02fe553f72a39b7a9fea820e023268467d7ab6"
 dependencies = [
  "aws-smithy-eventstream",
  "aws-smithy-runtime-api",
@@ -676,9 +678,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-runtime"
-version = "1.6.2"
+version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce87155eba55e11768b8c1afa607f3e864ae82f03caf63258b37455b0ad02537"
+checksum = "d1ce695746394772e7000b39fe073095db6d45a862d0767dd5ad0ac0d7f8eb87"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-http",
@@ -703,9 +705,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-runtime-api"
-version = "1.7.1"
+version = "1.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30819352ed0a04ecf6a2f3477e344d2d1ba33d43e0f09ad9047c12e0d923616f"
+checksum = "e086682a53d3aa241192aa110fa8dfce98f2f5ac2ead0de84d41582c7e8fdb96"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-types",
@@ -720,9 +722,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-types"
-version = "1.2.0"
+version = "1.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfe321a6b21f5d8eabd0ade9c55d3d0335f3c3157fc2b3e87f05f34b539e4df5"
+checksum = "147100a7bea70fa20ef224a6bad700358305f5dc0f84649c53769761395b355b"
 dependencies = [
  "base64-simd",
  "bytes",
@@ -746,9 +748,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-xml"
-version = "0.60.8"
+version = "0.60.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d123fbc2a4adc3c301652ba8e149bf4bc1d1725affb9784eb20c953ace06bf55"
+checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc"
 dependencies = [
  "xmlparser",
 ]
@@ -769,14 +771,14 @@ dependencies = [
 
 [[package]]
 name = "axum"
-version = "0.7.5"
+version = "0.7.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf"
+checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae"
 dependencies = [
  "async-trait",
  "axum-core",
  "axum-macros",
- "base64 0.21.7",
+ "base64 0.22.1",
  "bytes",
  "futures-util",
  "http 1.1.0",
@@ -800,7 +802,7 @@ dependencies = [
  "sync_wrapper 1.0.1",
  "tokio",
  "tokio-tungstenite",
- "tower",
+ "tower 0.5.1",
  "tower-layer",
  "tower-service",
  "tracing",
@@ -808,9 +810,9 @@ dependencies = [
 
 [[package]]
 name = "axum-core"
-version = "0.4.3"
+version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3"
+checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
 dependencies = [
  "async-trait",
  "bytes",
@@ -821,7 +823,7 @@ dependencies = [
  "mime",
  "pin-project-lite",
  "rustversion",
- "sync_wrapper 0.1.2",
+ "sync_wrapper 1.0.1",
  "tower-layer",
  "tower-service",
  "tracing",
@@ -829,11 +831,10 @@ dependencies = [
 
 [[package]]
 name = "axum-macros"
-version = "0.4.1"
+version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa"
+checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
 dependencies = [
- "heck 0.4.1",
  "proc-macro2",
  "quote",
  "syn 2.0.77",
@@ -871,25 +872,28 @@ dependencies = [
  "http 1.1.0",
  "importer-resolver",
  "importer-service",
+ "integration-service",
  "itertools 0.13.0",
  "logs-wheel",
  "media-models",
  "migrations",
  "miscellaneous-resolver",
  "miscellaneous-service",
+ "moka",
  "openidconnect",
- "reqwest 0.12.7",
+ "reqwest 0.12.8",
  "router-resolver",
  "schematic",
  "sea-orm",
  "sea-orm-migration",
  "serde",
  "serde_json",
- "serde_with 3.9.0",
+ "serde_with 3.11.0",
  "statistics-resolver",
  "statistics-service",
+ "supporting-service",
  "tokio",
- "tower",
+ "tower 0.4.13",
  "tower-http",
  "tracing",
  "tracing-subscriber",
@@ -1030,11 +1034,11 @@ dependencies = [
 
 [[package]]
 name = "block-buffer"
-version = "0.11.0-rc.0"
+version = "0.11.0-rc.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17092d478f4fadfb35a7e082f62e49f0907fdf048801d9d706277e34f9df8a78"
+checksum = "8969801e57d15e15bc4d7cdc5600dc15ca06a9a62b622bd4871c2d21d8aeb42d"
 dependencies = [
- "crypto-common 0.2.0-rc.0",
+ "crypto-common 0.2.0-rc.1",
 ]
 
 [[package]]
@@ -1108,9 +1112,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
 
 [[package]]
 name = "bytes"
-version = "1.6.1"
+version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952"
+checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
 dependencies = [
  "serde",
 ]
@@ -1149,9 +1153,12 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.1.6"
+version = "1.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f"
+checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48"
+dependencies = [
+ "shlex",
+]
 
 [[package]]
 name = "cfg-if"
@@ -1182,9 +1189,9 @@ dependencies = [
 
 [[package]]
 name = "chrono-tz"
-version = "0.9.0"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb"
+checksum = "cd6dd8046d00723a59a2f8c5f295c515b9bb9a331ee4f8f3d4dd49e428acd3b6"
 dependencies = [
  "chrono",
  "chrono-tz-build",
@@ -1193,12 +1200,11 @@ dependencies = [
 
 [[package]]
 name = "chrono-tz-build"
-version = "0.3.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1"
+checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7"
 dependencies = [
  "parse-zoneinfo",
- "phf 0.11.2",
  "phf_codegen 0.11.2",
 ]
 
@@ -1214,9 +1220,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.9"
+version = "4.5.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462"
+checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -1224,9 +1230,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.9"
+version = "4.5.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942"
+checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
 dependencies = [
  "anstream",
  "anstyle",
@@ -1236,9 +1242,9 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "4.5.8"
+version = "4.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
+checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
 dependencies = [
  "heck 0.5.0",
  "proc-macro2",
@@ -1248,9 +1254,9 @@ dependencies = [
 
 [[package]]
 name = "clap_lex"
-version = "0.7.1"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
+checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
 
 [[package]]
 name = "collection-resolver"
@@ -1268,11 +1274,8 @@ dependencies = [
 name = "collection-service"
 version = "0.1.0"
 dependencies = [
- "apalis",
  "async-graphql",
- "background",
  "common-models",
- "config",
  "database-models",
  "database-utils",
  "dependent-models",
@@ -1281,13 +1284,14 @@ dependencies = [
  "migrations",
  "sea-orm",
  "sea-query",
+ "supporting-service",
 ]
 
 [[package]]
 name = "colorchoice"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
+checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
 
 [[package]]
 name = "combine"
@@ -1328,7 +1332,7 @@ dependencies = [
  "sea-orm",
  "serde",
  "serde_json",
- "serde_with 3.9.0",
+ "serde_with 3.11.0",
  "strum",
 ]
 
@@ -1340,7 +1344,7 @@ dependencies = [
  "compile-time",
  "const-str",
  "env-utils",
- "reqwest 0.12.7",
+ "reqwest 0.12.8",
 ]
 
 [[package]]
@@ -1423,15 +1427,15 @@ dependencies = [
 
 [[package]]
 name = "core-foundation-sys"
-version = "0.8.6"
+version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.12"
+version = "0.2.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
+checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad"
 dependencies = [
  "libc",
 ]
@@ -1549,9 +1553,9 @@ dependencies = [
 
 [[package]]
 name = "crypto-common"
-version = "0.2.0-rc.0"
+version = "0.2.0-rc.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c070b79a496dccd931229780ad5bbedd535ceff6c3565605a8e440e18e1aa2b"
+checksum = "b0b8ce8218c97789f16356e7896b3714f26c2ee1079b79c0b7ae7064bb9089fa"
 dependencies = [
  "hybrid-array",
 ]
@@ -1692,7 +1696,7 @@ dependencies = [
  "sea-orm",
  "serde",
  "serde_json",
- "serde_with 3.9.0",
+ "serde_with 3.11.0",
  "user-models",
  "uuid",
 ]
@@ -1708,11 +1712,9 @@ dependencies = [
  "chrono",
  "common-models",
  "common-utils",
- "config",
  "database-models",
  "dependent-models",
  "enums",
- "file-storage-service",
  "fitness-models",
  "futures",
  "itertools 0.13.0",
@@ -1724,6 +1726,7 @@ dependencies = [
  "rust_decimal_macros",
  "sea-orm",
  "serde",
+ "supporting-service",
  "tracing",
  "user-models",
  "uuid",
@@ -1743,7 +1746,41 @@ dependencies = [
  "rust_decimal",
  "schematic",
  "serde",
- "serde_with 3.9.0",
+ "serde_with 3.11.0",
+]
+
+[[package]]
+name = "dependent-utils"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "apalis",
+ "application-utils",
+ "async-graphql",
+ "background",
+ "chrono",
+ "chrono-tz",
+ "common-models",
+ "common-utils",
+ "config",
+ "database-models",
+ "database-utils",
+ "dependent-models",
+ "enums",
+ "fitness-models",
+ "importer-models",
+ "itertools 0.13.0",
+ "media-models",
+ "nanoid",
+ "providers",
+ "rust_decimal",
+ "rust_decimal_macros",
+ "sea-orm",
+ "serde",
+ "supporting-service",
+ "tracing",
+ "traits",
+ "user-models",
 ]
 
 [[package]]
@@ -1832,8 +1869,8 @@ version = "0.11.0-pre.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf2e3d6615d99707295a9673e889bf363a04b2a466bd320c65a72536f7577379"
 dependencies = [
- "block-buffer 0.11.0-rc.0",
- "crypto-common 0.2.0-rc.0",
+ "block-buffer 0.11.0-rc.1",
+ "crypto-common 0.2.0-rc.1",
  "subtle",
 ]
 
@@ -2075,9 +2112,9 @@ dependencies = [
 
 [[package]]
 name = "email_address"
-version = "0.2.7"
+version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46b7a0ac6570e31bfe2c6cf575a576a55af9893d1a6b30b4444e6e90b216bb84"
+checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
 
 [[package]]
 name = "encode_unicode"
@@ -2225,19 +2262,18 @@ dependencies = [
  "chrono",
  "common-models",
  "common-utils",
- "config",
  "database-models",
  "database-utils",
  "dependent-models",
  "enums",
- "file-storage-service",
  "fitness-models",
  "media-models",
  "mime_guess",
  "nanoid",
- "reqwest 0.12.7",
+ "reqwest 0.12.8",
  "sea-orm",
  "struson",
+ "supporting-service",
  "tokio",
  "tokio-util",
 ]
@@ -2319,7 +2355,7 @@ dependencies = [
  "sea-orm",
  "serde",
  "serde_json",
- "serde_with 3.9.0",
+ "serde_with 3.11.0",
 ]
 
 [[package]]
@@ -2347,23 +2383,21 @@ dependencies = [
  "background",
  "common-models",
  "common-utils",
- "config",
  "const-str",
  "database-models",
  "database-utils",
  "dependent-models",
+ "dependent-utils",
  "enums",
- "file-storage-service",
  "fitness-models",
  "itertools 0.13.0",
  "migrations",
  "nanoid",
- "reqwest 0.12.7",
- "rust_decimal",
- "rust_decimal_macros",
+ "reqwest 0.12.8",
  "sea-orm",
  "sea-query",
  "slug",
+ "supporting-service",
  "tracing",
 ]
 
@@ -2436,9 +2470,9 @@ dependencies = [
 
 [[package]]
 name = "futures"
-version = "0.3.30"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -2451,9 +2485,9 @@ dependencies = [
 
 [[package]]
 name = "futures-channel"
-version = "0.3.30"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
 dependencies = [
  "futures-core",
  "futures-sink",
@@ -2461,15 +2495,15 @@ dependencies = [
 
 [[package]]
 name = "futures-core"
-version = "0.3.30"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
 
 [[package]]
 name = "futures-executor"
-version = "0.3.30"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
 dependencies = [
  "futures-core",
  "futures-task",
@@ -2489,9 +2523,9 @@ dependencies = [
 
 [[package]]
 name = "futures-io"
-version = "0.3.30"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
 
 [[package]]
 name = "futures-lite"
@@ -2508,9 +2542,9 @@ dependencies = [
 
 [[package]]
 name = "futures-macro"
-version = "0.3.30"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2528,15 +2562,15 @@ dependencies = [
 
 [[package]]
 name = "futures-sink"
-version = "0.3.30"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
 
 [[package]]
 name = "futures-task"
-version = "0.3.30"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
 
 [[package]]
 name = "futures-timer"
@@ -2546,9 +2580,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
 
 [[package]]
 name = "futures-util"
-version = "0.3.30"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -2742,9 +2776,9 @@ dependencies = [
 
 [[package]]
 name = "h2"
-version = "0.4.5"
+version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab"
+checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205"
 dependencies = [
  "atomic-waker",
  "bytes",
@@ -2828,6 +2862,12 @@ version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
 
+[[package]]
+name = "hermit-abi"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
+
 [[package]]
 name = "hex"
 version = "0.4.3"
@@ -3005,7 +3045,7 @@ dependencies = [
  "bytes",
  "futures-channel",
  "futures-util",
- "h2 0.4.5",
+ "h2 0.4.6",
  "http 1.1.0",
  "http-body 1.0.1",
  "httparse",
@@ -3043,7 +3083,7 @@ dependencies = [
  "http 1.1.0",
  "hyper 1.4.1",
  "hyper-util",
- "rustls 0.23.11",
+ "rustls 0.23.12",
  "rustls-pki-types",
  "tokio",
  "tokio-rustls 0.26.0",
@@ -3081,9 +3121,9 @@ dependencies = [
 
 [[package]]
 name = "hyper-util"
-version = "0.1.6"
+version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956"
+checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9"
 dependencies = [
  "bytes",
  "futures-channel",
@@ -3094,7 +3134,7 @@ dependencies = [
  "pin-project-lite",
  "socket2",
  "tokio",
- "tower",
+ "tower 0.4.13",
  "tower-service",
  "tracing",
 ]
@@ -3307,32 +3347,30 @@ dependencies = [
  "csv",
  "data-encoding",
  "database-models",
- "database-utils",
  "dependent-models",
+ "dependent-utils",
  "enum_meta",
  "enums",
  "env-utils",
  "fitness-models",
- "fitness-service",
  "flate2",
  "importer-models",
  "itertools 0.13.0",
  "media-models",
- "miscellaneous-service",
  "providers",
  "regex",
- "reqwest 0.12.7",
+ "reqwest 0.12.8",
  "rust_decimal",
  "rust_decimal_macros",
  "sea-orm",
  "serde",
  "serde-xml-rs",
  "serde_json",
- "serde_with 3.9.0",
+ "serde_with 3.11.0",
  "specific-models",
+ "supporting-service",
  "tracing",
  "traits",
- "user-models",
 ]
 
 [[package]]
@@ -3381,17 +3419,21 @@ dependencies = [
  "anyhow",
  "application-utils",
  "async-graphql",
+ "chrono",
  "common-models",
  "common-utils",
  "database-models",
  "database-utils",
+ "dependent-models",
+ "dependent-utils",
  "enums",
  "eventsource-stream",
+ "itertools 0.13.0",
  "media-models",
  "providers",
  "radarr-api-rs",
  "regex",
- "reqwest 0.12.7",
+ "reqwest 0.12.8",
  "rust_decimal",
  "rust_decimal_macros",
  "sea-orm",
@@ -3400,9 +3442,11 @@ dependencies = [
  "serde_json",
  "sonarr-api-rs",
  "specific-models",
+ "supporting-service",
  "tokio",
  "tracing",
  "traits",
+ "uuid",
 ]
 
 [[package]]
@@ -3413,11 +3457,11 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
 
 [[package]]
 name = "is-terminal"
-version = "0.4.12"
+version = "0.4.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b"
+checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b"
 dependencies = [
- "hermit-abi",
+ "hermit-abi 0.4.0",
  "libc",
  "windows-sys 0.52.0",
 ]
@@ -3430,9 +3474,9 @@ checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
 
 [[package]]
 name = "is_terminal_polyfill"
-version = "1.70.0"
+version = "1.70.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
 
 [[package]]
 name = "isolang"
@@ -3469,9 +3513,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
 
 [[package]]
 name = "js-sys"
-version = "0.3.69"
+version = "0.3.70"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
+checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -3497,7 +3541,7 @@ dependencies = [
  "chrono",
  "jsonwebtoken",
  "serde",
- "serde_with 3.9.0",
+ "serde_with 3.11.0",
  "uuid",
 ]
 
@@ -3537,9 +3581,9 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.155"
+version = "0.2.158"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
+checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
 
 [[package]]
 name = "libm"
@@ -3608,9 +3652,9 @@ dependencies = [
 
 [[package]]
 name = "lru"
-version = "0.12.3"
+version = "0.12.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
+checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904"
 dependencies = [
  "hashbrown 0.14.5",
 ]
@@ -3623,9 +3667,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
 
 [[package]]
 name = "markdown"
-version = "1.0.0-alpha.18"
+version = "1.0.0-alpha.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e61c5c85b392273c4d4ea546e6399ace3e3db172ab01b6de8f3d398d1dbd2ec"
+checksum = "a6491e6c702bf7e3b24e769d800746d5f2c06a6c6a2db7992612e0f429029e81"
 dependencies = [
  "unicode-id",
 ]
@@ -3684,7 +3728,7 @@ dependencies = [
  "sea-orm",
  "serde",
  "serde_json",
- "serde_with 3.9.0",
+ "serde_with 3.11.0",
  "strum",
 ]
 
@@ -3770,11 +3814,11 @@ dependencies = [
 
 [[package]]
 name = "mio"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4"
+checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
 dependencies = [
- "hermit-abi",
+ "hermit-abi 0.3.9",
  "libc",
  "wasi",
  "windows-sys 0.52.0",
@@ -3800,25 +3844,20 @@ dependencies = [
  "application-utils",
  "async-graphql",
  "background",
- "cache-service",
  "chrono",
- "chrono-tz",
  "common-models",
  "common-utils",
- "config",
  "database-models",
  "database-utils",
  "dependent-models",
+ "dependent-utils",
  "enums",
  "env-utils",
- "file-storage-service",
  "futures",
- "integration-service",
  "itertools 0.13.0",
  "markdown",
  "media-models",
  "migrations",
- "moka",
  "nanoid",
  "notification-service",
  "providers",
@@ -3829,6 +3868,7 @@ dependencies = [
  "serde",
  "serde_json",
  "slug",
+ "supporting-service",
  "tokio",
  "tracing",
  "traits",
@@ -3929,7 +3969,7 @@ dependencies = [
  "config",
  "convert_case",
  "lettre",
- "reqwest 0.12.7",
+ "reqwest 0.12.8",
  "serde",
  "serde_json",
  "tracing",
@@ -4021,9 +4061,9 @@ dependencies = [
 
 [[package]]
 name = "object"
-version = "0.36.1"
+version = "0.36.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce"
+checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9"
 dependencies = [
  "memchr",
 ]
@@ -4059,7 +4099,7 @@ dependencies = [
  "serde_json",
  "serde_path_to_error",
  "serde_plain",
- "serde_with 3.9.0",
+ "serde_with 3.11.0",
  "sha2",
  "subtle",
  "thiserror",
@@ -4499,9 +4539,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
 
 [[package]]
 name = "ppv-lite86"
-version = "0.2.17"
+version = "0.2.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
+dependencies = [
+ "zerocopy",
+]
 
 [[package]]
 name = "precomputed-hash"
@@ -4598,7 +4641,7 @@ dependencies = [
  "media-models",
  "paginate",
  "rand 0.9.0-alpha.2",
- "reqwest 0.12.7",
+ "reqwest 0.12.8",
  "rust_decimal",
  "rust_decimal_macros",
  "rust_iso3166",
@@ -4606,7 +4649,7 @@ dependencies = [
  "sea-orm",
  "serde",
  "serde_json",
- "serde_with 3.9.0",
+ "serde_with 3.11.0",
  "strum",
  "tracing",
  "traits",
@@ -4755,9 +4798,9 @@ dependencies = [
 
 [[package]]
 name = "raw-cpuid"
-version = "11.1.0"
+version = "11.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d"
+checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0"
 dependencies = [
  "bitflags 2.6.0",
 ]
@@ -4805,9 +4848,9 @@ dependencies = [
 
 [[package]]
 name = "redox_users"
-version = "0.4.5"
+version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
 dependencies = [
  "getrandom",
  "libredox",
@@ -4816,14 +4859,14 @@ dependencies = [
 
 [[package]]
 name = "regex"
-version = "1.10.5"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
+checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-automata 0.4.7",
- "regex-syntax 0.8.4",
+ "regex-automata 0.4.8",
+ "regex-syntax 0.8.5",
 ]
 
 [[package]]
@@ -4837,13 +4880,13 @@ dependencies = [
 
 [[package]]
 name = "regex-automata"
-version = "0.4.7"
+version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
+checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-syntax 0.8.4",
+ "regex-syntax 0.8.5",
 ]
 
 [[package]]
@@ -4860,9 +4903,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
 
 [[package]]
 name = "regex-syntax"
-version = "0.8.4"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
 
 [[package]]
 name = "rend"
@@ -4920,9 +4963,9 @@ dependencies = [
 
 [[package]]
 name = "reqwest"
-version = "0.12.7"
+version = "0.12.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63"
+checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b"
 dependencies = [
  "base64 0.22.1",
  "bytes",
@@ -4930,7 +4973,7 @@ dependencies = [
  "futures-channel",
  "futures-core",
  "futures-util",
- "h2 0.4.5",
+ "h2 0.4.6",
  "http 1.1.0",
  "http-body 1.0.1",
  "http-body-util",
@@ -4947,12 +4990,12 @@ dependencies = [
  "once_cell",
  "percent-encoding",
  "pin-project-lite",
- "rustls-pemfile 2.1.2",
+ "rustls-pemfile 2.1.3",
  "serde",
  "serde_json",
  "serde_urlencoded",
  "sync_wrapper 1.0.1",
- "system-configuration 0.6.1",
+ "system-configuration 0.6.0",
  "tokio",
  "tokio-native-tls",
  "tokio-util",
@@ -5003,9 +5046,9 @@ dependencies = [
 
 [[package]]
 name = "rkyv"
-version = "0.7.44"
+version = "0.7.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0"
+checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b"
 dependencies = [
  "bitvec",
  "bytecheck",
@@ -5021,9 +5064,9 @@ dependencies = [
 
 [[package]]
 name = "rkyv_derive"
-version = "0.7.44"
+version = "0.7.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65"
+checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -5039,7 +5082,7 @@ dependencies = [
  "axum",
  "common-utils",
  "config",
- "miscellaneous-service",
+ "integration-service",
  "nanoid",
  "serde_json",
  "tracing",
@@ -5145,9 +5188,9 @@ dependencies = [
 
 [[package]]
 name = "rustls"
-version = "0.23.11"
+version = "0.23.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0"
+checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044"
 dependencies = [
  "once_cell",
  "rustls-pki-types",
@@ -5179,9 +5222,9 @@ dependencies = [
 
 [[package]]
 name = "rustls-pemfile"
-version = "2.1.2"
+version = "2.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
+checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425"
 dependencies = [
  "base64 0.22.1",
  "rustls-pki-types",
@@ -5189,9 +5232,9 @@ dependencies = [
 
 [[package]]
 name = "rustls-pki-types"
-version = "1.7.0"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
+checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0"
 
 [[package]]
 name = "rustls-webpki"
@@ -5237,14 +5280,14 @@ dependencies = [
 
 [[package]]
 name = "schematic"
-version = "0.17.4"
+version = "0.17.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "414b8235f6cf6a13ed2a42df1a5f69006280ca860b2a0762afb4de8c4367db70"
+checksum = "9803bd9e6cccb1cc9cec6943f34ba93aa6cf7b6f183ee20a43457934dbc2b686"
 dependencies = [
  "garde",
  "indexmap 2.5.0",
  "miette",
- "reqwest 0.12.7",
+ "reqwest 0.12.8",
  "schematic_macros",
  "schematic_types",
  "serde",
@@ -5554,9 +5597,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
 
 [[package]]
 name = "serde"
-version = "1.0.209"
+version = "1.0.210"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
+checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
 dependencies = [
  "serde_derive",
 ]
@@ -5585,9 +5628,9 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.209"
+version = "1.0.210"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
+checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -5596,9 +5639,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.127"
+version = "1.0.128"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
+checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
 dependencies = [
  "indexmap 2.5.0",
  "itoa",
@@ -5665,9 +5708,9 @@ dependencies = [
 
 [[package]]
 name = "serde_with"
-version = "3.9.0"
+version = "3.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857"
+checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817"
 dependencies = [
  "base64 0.22.1",
  "chrono",
@@ -5677,7 +5720,7 @@ dependencies = [
  "serde",
  "serde_derive",
  "serde_json",
- "serde_with_macros 3.9.0",
+ "serde_with_macros 3.11.0",
  "time",
 ]
 
@@ -5695,9 +5738,9 @@ dependencies = [
 
 [[package]]
 name = "serde_with_macros"
-version = "3.9.0"
+version = "3.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350"
+checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d"
 dependencies = [
  "darling",
  "proc-macro2",
@@ -5764,6 +5807,12 @@ dependencies = [
  "lazy_static",
 ]
 
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
 [[package]]
 name = "signal-hook-registry"
 version = "1.4.2"
@@ -5816,9 +5865,9 @@ dependencies = [
 
 [[package]]
 name = "slug"
-version = "0.1.5"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4"
+checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724"
 dependencies = [
  "deunicode",
  "wasm-bindgen",
@@ -5846,10 +5895,10 @@ version = "3.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a0f85e838c6093957978793ece2c0893c838da8dd5bcab0ff6f57fcaa8fc8ee9"
 dependencies = [
- "reqwest 0.12.7",
+ "reqwest 0.12.8",
  "serde",
  "serde_json",
- "serde_with 3.9.0",
+ "serde_with 3.11.0",
  "url",
  "uuid",
 ]
@@ -6132,9 +6181,9 @@ dependencies = [
 
 [[package]]
 name = "starbase_styles"
-version = "0.4.3"
+version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21a18837e83f7754c667fddb2c954ce27a068f9d85053a88d41db4a81435b5fe"
+checksum = "44854a14e28e3b1d602d802576162380504df73efae50d4b901934d25579524b"
 dependencies = [
  "dirs",
  "owo-colors",
@@ -6174,6 +6223,7 @@ dependencies = [
  "dependent-models",
  "media-models",
  "sea-orm",
+ "supporting-service",
  "tracing",
 ]
 
@@ -6222,9 +6272,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 
 [[package]]
 name = "strum"
-version = "0.26.2"
+version = "0.26.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
+checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
 dependencies = [
  "strum_macros",
 ]
@@ -6260,11 +6310,27 @@ version = "2.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
 
+[[package]]
+name = "supporting-service"
+version = "0.1.0"
+dependencies = [
+ "apalis",
+ "background",
+ "cache-service",
+ "chrono-tz",
+ "config",
+ "file-storage-service",
+ "media-models",
+ "moka",
+ "openidconnect",
+ "sea-orm",
+]
+
 [[package]]
 name = "supports-color"
-version = "3.0.1"
+version = "3.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8775305acf21c96926c900ad056abeef436701108518cf890020387236ac5a77"
+checksum = "9829b314621dfc575df4e409e79f9d6a66a3bd707ab73f23cb4aa3a854ac854f"
 dependencies = [
  "is_ci",
 ]
@@ -6342,9 +6408,9 @@ dependencies = [
 
 [[package]]
 name = "system-configuration"
-version = "0.6.1"
+version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
+checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42"
 dependencies = [
  "bitflags 2.6.0",
  "core-foundation",
@@ -6385,14 +6451,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
 
 [[package]]
 name = "tempfile"
-version = "3.10.1"
+version = "3.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
+checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
 dependencies = [
  "cfg-if",
  "fastrand",
+ "once_cell",
  "rustix",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -6505,9 +6572,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
 
 [[package]]
 name = "tokio"
-version = "1.39.2"
+version = "1.40.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1"
+checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
 dependencies = [
  "backtrace",
  "bytes",
@@ -6569,7 +6636,7 @@ version = "0.26.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
 dependencies = [
- "rustls 0.23.11",
+ "rustls 0.23.12",
  "rustls-pki-types",
  "tokio",
 ]
@@ -6587,9 +6654,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-tungstenite"
-version = "0.21.0"
+version = "0.24.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
+checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
 dependencies = [
  "futures-util",
  "log",
@@ -6673,6 +6740,22 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "tower"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper 0.1.2",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
 [[package]]
 name = "tower-http"
 version = "0.5.2"
@@ -6693,15 +6776,15 @@ dependencies = [
 
 [[package]]
 name = "tower-layer"
-version = "0.3.2"
+version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
 
 [[package]]
 name = "tower-service"
-version = "0.3.2"
+version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
 
 [[package]]
 name = "tracing"
@@ -6810,9 +6893,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
 
 [[package]]
 name = "tungstenite"
-version = "0.21.0"
+version = "0.24.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
+checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
 dependencies = [
  "byteorder",
  "bytes",
@@ -6823,7 +6906,6 @@ dependencies = [
  "rand 0.8.5",
  "sha1",
  "thiserror",
- "url",
  "utf-8",
 ]
 
@@ -6888,9 +6970,9 @@ dependencies = [
 
 [[package]]
 name = "unicode-properties"
-version = "0.1.1"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291"
+checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524"
 
 [[package]]
 name = "unicode-segmentation"
@@ -6972,7 +7054,7 @@ dependencies = [
  "sea-orm",
  "serde",
  "serde_json",
- "serde_with 3.9.0",
+ "serde_with 3.11.0",
  "strum",
 ]
 
@@ -6994,15 +7076,12 @@ dependencies = [
 name = "user-service"
 version = "0.1.0"
 dependencies = [
- "apalis",
  "application-utils",
  "argon2",
  "async-graphql",
- "background",
  "chrono",
  "common-models",
  "common-utils",
- "config",
  "database-models",
  "database-utils",
  "dependent-models",
@@ -7017,6 +7096,7 @@ dependencies = [
  "openidconnect",
  "sea-orm",
  "serde_json",
+ "supporting-service",
  "tracing",
  "user-models",
 ]
@@ -7069,9 +7149,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 
 [[package]]
 name = "version_check"
-version = "0.9.4"
+version = "0.9.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
 
 [[package]]
 name = "void"
@@ -7108,19 +7188,20 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.92"
+version = "0.2.93"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
+checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
 dependencies = [
  "cfg-if",
+ "once_cell",
  "wasm-bindgen-macro",
 ]
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.92"
+version = "0.2.93"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
+checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
 dependencies = [
  "bumpalo",
  "log",
@@ -7133,9 +7214,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.42"
+version = "0.4.43"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
+checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
 dependencies = [
  "cfg-if",
  "js-sys",
@@ -7145,9 +7226,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.92"
+version = "0.2.93"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
+checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -7155,9 +7236,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.92"
+version = "0.2.93"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
+checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -7168,9 +7249,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.92"
+version = "0.2.93"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
+checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
 
 [[package]]
 name = "wasm-streams"
@@ -7187,9 +7268,9 @@ dependencies = [
 
 [[package]]
 name = "web-sys"
-version = "0.3.69"
+version = "0.3.70"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
+checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -7310,6 +7391,15 @@ dependencies = [
  "windows-targets 0.52.6",
 ]
 
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
 [[package]]
 name = "windows-targets"
 version = "0.48.5"
@@ -7482,9 +7572,9 @@ dependencies = [
 
 [[package]]
 name = "xml-rs"
-version = "0.8.20"
+version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193"
+checksum = "539a77ee7c0de333dcc6da69b177380a0b81e0dacfa4f7344c465a36871ee601"
 
 [[package]]
 name = "xmlparser"
@@ -7522,6 +7612,7 @@ version = "0.7.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
 dependencies = [
+ "byteorder",
  "zerocopy-derive",
 ]
 
diff --git a/Cargo.toml b/Cargo.toml
index 46bf556e37..b40a3947c7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -34,31 +34,33 @@ members = [
     "crates/services/miscellaneous",
     "crates/services/notification",
     "crates/services/statistics",
+    "crates/services/supporting",
     "crates/services/user",
     "crates/traits",
     "crates/utils/application",
     "crates/utils/common",
     "crates/utils/database",
+    "crates/utils/dependent",
     "crates/utils/env",
 ]
 resolver = "2"
 
 [workspace.dependencies]
-anyhow = "=1.0.82"
+anyhow = "=1.0.89"
 apalis = { version = "=0.5.3", features = ["cron", "limit"] }
 argon2 = "=0.6.0-pre.1"
-async-graphql = { version = "=7.0.9", features = [
+async-graphql = { version = "=7.0.11", features = [
     "chrono",
     "decimal",
     "log",
     "tracing",
 ] }
-async-trait = "=0.1.81"
-aws-sdk-s3 = { version = "=1.42.0", features = ["behavior-version-latest"] }
-axum = { version = "=0.7.5", features = ["macros", "multipart"] }
+async-trait = "=0.1.83"
+aws-sdk-s3 = { version = "=1.54.0", features = ["behavior-version-latest"] }
+axum = { version = "=0.7.7", features = ["macros", "multipart"] }
 boilermates = "=0.3.0"
 chrono = "=0.4.38"
-chrono-tz = "=0.9.0"
+chrono-tz = "=0.10.0"
 const-str = "=0.5.7"
 convert_case = "=0.6.0"
 derive_more = { version = "=1.0.0", features = [
@@ -68,15 +70,16 @@ derive_more = { version = "=1.0.0", features = [
 ], default-features = false }
 enum_meta = "=0.7.0"
 itertools = "=0.13.0"
-futures = "=0.3.30"
+futures = "=0.3.31"
 hashbag = "=0.1.12"
-markdown = "=1.0.0-alpha.18"
+markdown = "=1.0.0-alpha.21"
+moka = { version = "=0.12.8", features = ["future"] }
 nanoid = "=0.4.0"
 openidconnect = "=3.5.0"
-regex = "=1.10.5"
+regex = "=1.11.0"
 rust_decimal = "=1.36.0"
 rust_decimal_macros = "=1.36.0"
-schematic = { version = "=0.17.4", features = [
+schematic = { version = "=0.17.5", features = [
     "config",
     "env",
     "json",
@@ -103,13 +106,13 @@ sea-orm = { version = "=1.0.1", features = [
 ], default-features = false }
 sea-orm-migration = "=1.0.1"
 sea-query = "=0.31.1"
-serde = { version = "=1.0.209", features = ["derive"] }
-serde_json = "=1.0.127"
-serde_with = { version = "=3.9.0", features = ["chrono_0_4"] }
-slug = "=0.1.5"
-strum = { version = "=0.26.2", features = ["derive"] }
-reqwest = { version = "=0.12.7", features = ["json", "stream"] }
-tokio = { version = "=1.39.2", features = ["full"] }
+serde = { version = "=1.0.210", features = ["derive"] }
+serde_json = "=1.0.128"
+serde_with = { version = "=3.11.0", features = ["chrono_0_4"] }
+slug = "=0.1.6"
+strum = { version = "=0.26.3", features = ["derive"] }
+reqwest = { version = "=0.12.8", features = ["json", "stream"] }
+tokio = { version = "=1.40.0", features = ["full"] }
 tracing = { version = "=0.1.40", features = ["attributes"] }
 uuid = { version = "=1.10.0", features = ["v4"], default-features = false }
 
diff --git a/apps/backend/Cargo.toml b/apps/backend/Cargo.toml
index fb0ce386d0..018ce63dfa 100644
--- a/apps/backend/Cargo.toml
+++ b/apps/backend/Cargo.toml
@@ -10,7 +10,7 @@ anyhow = { workspace = true }
 apalis = { workspace = true }
 application-utils = { path = "../../crates/utils/application" }
 async-graphql = { workspace = true }
-async-graphql-axum = "=7.0.9"
+async-graphql-axum = "=7.0.11"
 axum = { workspace = true }
 aws-sdk-s3 = { workspace = true }
 background = { path = "../../crates/background" }
@@ -34,9 +34,11 @@ fitness-service = { path = "../../crates/services/fitness" }
 http = "=1.1.0"
 importer-resolver = { path = "../../crates/resolvers/importer" }
 importer-service = { path = "../../crates/services/importer" }
+integration-service = { path = "../../crates/services/integration" }
 itertools = { workspace = true }
 logs-wheel = "=0.3.1"
 media-models = { path = "../../crates/models/media" }
+moka = { workspace = true }
 migrations = { path = "../../crates/migrations" }
 miscellaneous-resolver = { path = "../../crates/resolvers/miscellaneous" }
 miscellaneous-service = { path = "../../crates/services/miscellaneous" }
@@ -51,6 +53,7 @@ serde_with = { workspace = true }
 schematic = { workspace = true }
 statistics-resolver = { path = "../../crates/resolvers/statistics" }
 statistics-service = { path = "../../crates/services/statistics" }
+supporting-service = { path = "../../crates/services/supporting" }
 tokio = { workspace = true }
 tower = { version = "=0.4.13", features = ["buffer"] }
 tower-http = { version = "=0.5.2", features = ["catch-panic", "cors", "trace"] }
diff --git a/apps/backend/src/common.rs b/apps/backend/src/common.rs
index 990435c413..0601cdeac7 100644
--- a/apps/backend/src/common.rs
+++ b/apps/backend/src/common.rs
@@ -12,6 +12,7 @@ use axum::{
 };
 use background::{ApplicationJob, CoreApplicationJob};
 use cache_service::CacheService;
+use chrono::Duration;
 use collection_resolver::{CollectionMutation, CollectionQuery};
 use collection_service::CollectionService;
 use common_utils::{ryot_log, FRONTEND_OAUTH_ENDPOINT};
@@ -23,9 +24,11 @@ use fitness_resolver::{ExerciseMutation, ExerciseQuery};
 use fitness_service::ExerciseService;
 use importer_resolver::{ImporterMutation, ImporterQuery};
 use importer_service::ImporterService;
+use integration_service::IntegrationService;
 use itertools::Itertools;
 use miscellaneous_resolver::{MiscellaneousMutation, MiscellaneousQuery};
 use miscellaneous_service::MiscellaneousService;
+use moka::future::Cache;
 use openidconnect::{
     core::{CoreClient, CoreProviderMetadata},
     reqwest::async_http_client,
@@ -35,6 +38,7 @@ use router_resolver::{config_handler, graphql_playground, integration_webhook, u
 use sea_orm::DatabaseConnection;
 use statistics_resolver::StatisticsQuery;
 use statistics_service::StatisticsService;
+use supporting_service::SupportingService;
 use tower_http::{
     catch_panic::CatchPanicLayer as TowerCatchPanicLayer, cors::CorsLayer as TowerCorsLayer,
     trace::TraceLayer as TowerTraceLayer,
@@ -48,6 +52,7 @@ pub struct AppServices {
     pub importer_service: Arc<ImporterService>,
     pub exporter_service: Arc<ExporterService>,
     pub exercise_service: Arc<ExerciseService>,
+    pub integration_service: Arc<IntegrationService>,
     pub statistics_service: Arc<StatisticsService>,
     pub app_router: Router,
 }
@@ -62,63 +67,38 @@ pub async fn create_app_services(
     perform_core_application_job: &MemoryStorage<CoreApplicationJob>,
     timezone: chrono_tz::Tz,
 ) -> AppServices {
-    let timezone = Arc::new(timezone);
+    let oidc_client = create_oidc_client(&config).await;
     let file_storage_service = Arc::new(FileStorageService::new(
         s3_client,
         config.file_storage.s3_bucket_name.clone(),
     ));
-    let exercise_service = Arc::new(ExerciseService::new(
-        is_pro,
-        &db,
-        config.clone(),
-        file_storage_service.clone(),
-        perform_application_job,
-        perform_core_application_job,
-    ));
-    let oidc_client = Arc::new(create_oidc_client(&config).await);
-
-    let cache_service = Arc::new(CacheService::new(&db));
-    let collection_service = Arc::new(CollectionService::new(
-        &db,
-        config.clone(),
-        perform_core_application_job,
-    ));
-    let miscellaneous_service = Arc::new(
-        MiscellaneousService::new(
+    let cache_service = CacheService::new(&db);
+    let commit_cache = Cache::builder()
+        .time_to_live(Duration::try_hours(1).unwrap().to_std().unwrap())
+        .build();
+    let supporting_service = Arc::new(
+        SupportingService::new(
             is_pro,
-            oidc_client.is_some(),
             &db,
-            timezone.clone(),
+            timezone,
+            cache_service,
             config.clone(),
-            cache_service.clone(),
+            oidc_client,
+            commit_cache,
             file_storage_service.clone(),
             perform_application_job,
             perform_core_application_job,
         )
         .await,
     );
-    let user_service = Arc::new(UserService::new(
-        is_pro,
-        &db,
-        config.clone(),
-        perform_application_job,
-        oidc_client.clone(),
-    ));
-    let importer_service = Arc::new(ImporterService::new(
-        &db,
-        perform_application_job,
-        perform_core_application_job,
-        miscellaneous_service.clone(),
-        exercise_service.clone(),
-        timezone.clone(),
-    ));
-    let exporter_service = Arc::new(ExporterService::new(
-        &db,
-        config.clone(),
-        perform_application_job,
-        file_storage_service.clone(),
-    ));
-    let statistics_service = Arc::new(StatisticsService::new(&db));
+    let user_service = Arc::new(UserService(supporting_service.clone()));
+    let importer_service = Arc::new(ImporterService(supporting_service.clone()));
+    let exercise_service = Arc::new(ExerciseService(supporting_service.clone()));
+    let exporter_service = Arc::new(ExporterService(supporting_service.clone()));
+    let collection_service = Arc::new(CollectionService(supporting_service.clone()));
+    let statistics_service = Arc::new(StatisticsService(supporting_service.clone()));
+    let integration_service = Arc::new(IntegrationService(supporting_service.clone()));
+    let miscellaneous_service = Arc::new(MiscellaneousService(supporting_service.clone()));
     let schema = Schema::build(
         QueryRoot::default(),
         MutationRoot::default(),
@@ -163,7 +143,7 @@ pub async fn create_app_services(
         .route("/graphql", gql)
         .route("/upload", post(upload_file))
         .layer(Extension(config.clone()))
-        .layer(Extension(miscellaneous_service.clone()))
+        .layer(Extension(integration_service.clone()))
         .layer(Extension(schema))
         .layer(TowerTraceLayer::new_for_http())
         .layer(TowerCatchPanicLayer::new())
@@ -172,12 +152,13 @@ pub async fn create_app_services(
         ))
         .layer(cors);
     AppServices {
-        miscellaneous_service,
+        app_router,
         importer_service,
         exporter_service,
         exercise_service,
         statistics_service,
-        app_router,
+        integration_service,
+        miscellaneous_service,
     }
 }
 
diff --git a/apps/backend/src/job.rs b/apps/backend/src/job.rs
index 2e388ffa3f..a9a25df0b9 100644
--- a/apps/backend/src/job.rs
+++ b/apps/backend/src/job.rs
@@ -6,6 +6,7 @@ use common_utils::ryot_log;
 use exporter_service::ExporterService;
 use fitness_service::ExerciseService;
 use importer_service::ImporterService;
+use integration_service::IntegrationService;
 use media_models::CommitMediaInput;
 use miscellaneous_service::MiscellaneousService;
 use statistics_service::StatisticsService;
@@ -21,9 +22,9 @@ pub async fn background_jobs(
 
 pub async fn sync_integrations_data(
     _information: ScheduledJob,
-    misc_service: Data<Arc<MiscellaneousService>>,
+    integration_service: Data<Arc<IntegrationService>>,
 ) -> Result<(), Error> {
-    misc_service.sync_integrations_data().await.unwrap();
+    integration_service.yank_integrations_data().await.unwrap();
     Ok(())
 }
 
@@ -31,13 +32,14 @@ pub async fn sync_integrations_data(
 
 pub async fn perform_core_application_job(
     information: CoreApplicationJob,
+    integration_service: Data<Arc<IntegrationService>>,
     misc_service: Data<Arc<MiscellaneousService>>,
 ) -> 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) => misc_service
+        CoreApplicationJob::SyncIntegrationsData(user_id) => integration_service
             .yank_integrations_data_for_user(&user_id)
             .await
             .is_ok(),
@@ -68,6 +70,7 @@ pub async fn perform_core_application_job(
 pub async fn perform_application_job(
     information: ApplicationJob,
     misc_service: Data<Arc<MiscellaneousService>>,
+    integration_service: Data<Arc<IntegrationService>>,
     importer_service: Data<Arc<ImporterService>>,
     exporter_service: Data<Arc<ExporterService>>,
     exercise_service: Data<Arc<ExerciseService>>,
@@ -129,7 +132,15 @@ pub async fn perform_application_job(
             .deploy_update_exercise_library_job()
             .await
             .is_ok(),
-        ApplicationJob::SyncIntegrationsData => misc_service.sync_integrations_data().await.is_ok(),
+        ApplicationJob::SyncIntegrationsData => {
+            integration_service.yank_integrations_data().await.is_ok()
+        }
+        ApplicationJob::HandleEntityAddedToCollectionEvent(user_id, collection_to_entity_id) => {
+            integration_service
+                .handle_entity_added_to_collection_event(user_id, collection_to_entity_id)
+                .await
+                .is_ok()
+        }
     };
     ryot_log!(
         trace,
diff --git a/apps/backend/src/main.rs b/apps/backend/src/main.rs
index 91566c723f..b79499c49c 100644
--- a/apps/backend/src/main.rs
+++ b/apps/backend/src/main.rs
@@ -198,12 +198,14 @@ async fn main() -> Result<()> {
 
     let importer_service_1 = app_services.importer_service.clone();
     let exporter_service_1 = app_services.exporter_service.clone();
-    let media_service_2 = app_services.miscellaneous_service.clone();
-    let media_service_3 = app_services.miscellaneous_service.clone();
-    let media_service_4 = app_services.miscellaneous_service.clone();
-    let media_service_5 = app_services.miscellaneous_service.clone();
+    let miscellaneous_service_2 = app_services.miscellaneous_service.clone();
+    let miscellaneous_service_4 = app_services.miscellaneous_service.clone();
+    let miscellaneous_service_5 = app_services.miscellaneous_service.clone();
     let exercise_service_1 = app_services.exercise_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 monitor = async {
         Monitor::<TokioExecutor>::new()
@@ -219,7 +221,7 @@ async fn main() -> Result<()> {
                         .into_stream(),
                     )
                     .layer(ApalisTraceLayer::new())
-                    .data(media_service_2.clone())
+                    .data(miscellaneous_service_2.clone())
                     .build_fn(background_jobs),
             )
             .register_with_count(
@@ -234,7 +236,7 @@ async fn main() -> Result<()> {
                         .into_stream(),
                     )
                     .layer(ApalisTraceLayer::new())
-                    .data(media_service_3.clone())
+                    .data(integration_service_1.clone())
                     .build_fn(sync_integrations_data),
             )
             // application jobs
@@ -242,7 +244,8 @@ async fn main() -> Result<()> {
                 1,
                 WorkerBuilder::new("perform_core_application_job")
                     .layer(ApalisTraceLayer::new())
-                    .data(media_service_5.clone())
+                    .data(miscellaneous_service_5.clone())
+                    .data(integration_service_2.clone())
                     .source(perform_core_application_job_storage)
                     .build_fn(perform_core_application_job),
             )
@@ -250,8 +253,9 @@ async fn main() -> Result<()> {
                 3,
                 WorkerBuilder::new("perform_application_job")
                     .data(importer_service_1.clone())
+                    .data(integration_service_3.clone())
                     .data(exporter_service_1.clone())
-                    .data(media_service_4.clone())
+                    .data(miscellaneous_service_4.clone())
                     .data(exercise_service_1.clone())
                     .data(statistics_service_1.clone())
                     .source(perform_application_job_storage)
diff --git a/apps/frontend/app/routes/_dashboard.tsx b/apps/frontend/app/routes/_dashboard.tsx
index 0552ae61c0..065f5165ef 100644
--- a/apps/frontend/app/routes/_dashboard.tsx
+++ b/apps/frontend/app/routes/_dashboard.tsx
@@ -246,11 +246,8 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 	const decodedCookie = jwtDecode<{
 		access_link?: { id: string; is_demo?: boolean };
 	}>(getAuthorizationCookie(request));
-	console.log(decodedCookie);
 	const isAccessLinkSession = Boolean(decodedCookie?.access_link);
-	console.log(isAccessLinkSession);
 	const isDemo = Boolean(decodedCookie?.access_link?.is_demo);
-	console.log(isDemo);
 
 	const shouldHaveUmami =
 		envData.FRONTEND_UMAMI_SCRIPT_URL &&
diff --git a/crates/background/src/lib.rs b/crates/background/src/lib.rs
index c093df6623..914cc10d69 100644
--- a/crates/background/src/lib.rs
+++ b/crates/background/src/lib.rs
@@ -37,6 +37,7 @@ pub enum ApplicationJob {
     PerformBackgroundTasks,
     UpdateExerciseLibrary,
     SyncIntegrationsData,
+    HandleEntityAddedToCollectionEvent(String, Uuid),
 }
 
 impl Message for ApplicationJob {
diff --git a/crates/enums/src/lib.rs b/crates/enums/src/lib.rs
index 76c09a1ea2..a118a35211 100644
--- a/crates/enums/src/lib.rs
+++ b/crates/enums/src/lib.rs
@@ -461,6 +461,7 @@ pub enum IntegrationProvider {
     Radarr,
     Sonarr,
     Komga,
+    GenericJson,
 }
 
 #[derive(
diff --git a/crates/models/database/src/exercise.rs b/crates/models/database/src/exercise.rs
index 9a95c932ea..c05b085c34 100644
--- a/crates/models/database/src/exercise.rs
+++ b/crates/models/database/src/exercise.rs
@@ -1,7 +1,5 @@
 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
 
-use std::sync::Arc;
-
 use application_utils::GraphqlRepresentation;
 use async_graphql::{InputObject, Result, SimpleObject};
 use async_trait::async_trait;
@@ -51,7 +49,7 @@ pub struct Model {
 impl GraphqlRepresentation for Model {
     async fn graphql_representation(
         self,
-        file_storage_service: &Arc<FileStorageService>,
+        file_storage_service: &FileStorageService,
     ) -> Result<Self> {
         let mut converted_exercise = self.clone();
         let mut images = vec![];
diff --git a/crates/models/database/src/workout.rs b/crates/models/database/src/workout.rs
index 7460842f69..a9f68ecc68 100644
--- a/crates/models/database/src/workout.rs
+++ b/crates/models/database/src/workout.rs
@@ -1,7 +1,5 @@
 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.1
 
-use std::sync::Arc;
-
 use application_utils::GraphqlRepresentation;
 use async_graphql::{Result, SimpleObject};
 use async_trait::async_trait;
@@ -13,7 +11,16 @@ use serde::{Deserialize, Serialize};
 
 /// A workout that was completed by the user.
 #[derive(
-    Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, SimpleObject, Schematic,
+    Clone,
+    Debug,
+    PartialEq,
+    DeriveEntityModel,
+    Eq,
+    Serialize,
+    Deserialize,
+    SimpleObject,
+    Schematic,
+    Default,
 )]
 #[sea_orm(table_name = "workout")]
 #[graphql(name = "Workout")]
@@ -39,7 +46,7 @@ pub struct Model {
 impl GraphqlRepresentation for Model {
     async fn graphql_representation(
         self,
-        file_storage_service: &Arc<FileStorageService>,
+        file_storage_service: &FileStorageService,
     ) -> Result<Self> {
         let mut cnv_workout = self.clone();
         if let Some(ref mut assets) = cnv_workout.information.assets {
diff --git a/crates/models/dependent/src/lib.rs b/crates/models/dependent/src/lib.rs
index a12bb1cce1..89d574a968 100644
--- a/crates/models/dependent/src/lib.rs
+++ b/crates/models/dependent/src/lib.rs
@@ -74,7 +74,7 @@ pub struct CompleteExport {
     /// Data about user's workouts.
     pub workouts: Option<Vec<ImportOrExportWorkoutItem>>,
     /// Data about user's media groups.
-    pub media_group: Option<Vec<media_models::ImportOrExportMediaGroupItem>>,
+    pub media_groups: Option<Vec<media_models::ImportOrExportMediaGroupItem>>,
     /// Data about user's exercises.
     pub exercises: Option<Vec<ImportOrExportExerciseItem>>,
     /// Data about user's workout templates.
@@ -205,13 +205,14 @@ pub struct UserMetadataDetails {
 
 #[derive(Debug, Default)]
 pub struct ImportResult {
-    pub collections: Vec<CreateOrUpdateCollectionInput>,
-    pub media: Vec<ImportOrExportMediaItem>,
-    pub media_groups: Vec<ImportOrExportMediaGroupItem>,
-    pub people: Vec<ImportOrExportPersonItem>,
-    pub measurements: Vec<user_measurement::Model>,
     pub workouts: Vec<UserWorkoutInput>,
     pub failed_items: Vec<ImportFailedItem>,
+    pub metadata: Vec<ImportOrExportMediaItem>,
+    pub people: Vec<ImportOrExportPersonItem>,
+    pub measurements: Vec<user_measurement::Model>,
+    pub metadata_groups: Vec<ImportOrExportMediaGroupItem>,
+    pub collections: Vec<CreateOrUpdateCollectionInput>,
+    pub application_workouts: Vec<ImportOrExportWorkoutItem>,
 }
 
 #[derive(Debug, Serialize, Deserialize, SimpleObject, Clone)]
diff --git a/crates/models/fitness/src/lib.rs b/crates/models/fitness/src/lib.rs
index 7dccd950fa..a86600448a 100644
--- a/crates/models/fitness/src/lib.rs
+++ b/crates/models/fitness/src/lib.rs
@@ -1,4 +1,4 @@
-use std::{collections::HashMap, sync::Arc};
+use std::collections::HashMap;
 
 use application_utils::GraphqlRepresentation;
 use async_graphql::{Enum, InputObject, Result as GraphqlResult, SimpleObject};
@@ -16,6 +16,24 @@ use sea_orm::{prelude::DateTimeUtc, FromJsonQueryResult, FromQueryResult};
 use serde::{Deserialize, Serialize};
 use serde_with::skip_serializing_none;
 
+pub const LOT_MAPPINGS: &[(ExerciseLot, &[WorkoutSetPersonalBest])] = &[
+    (ExerciseLot::Duration, &[WorkoutSetPersonalBest::Time]),
+    (
+        ExerciseLot::DistanceAndDuration,
+        &[WorkoutSetPersonalBest::Pace, WorkoutSetPersonalBest::Time],
+    ),
+    (
+        ExerciseLot::RepsAndWeight,
+        &[
+            WorkoutSetPersonalBest::Weight,
+            WorkoutSetPersonalBest::OneRm,
+            WorkoutSetPersonalBest::Volume,
+            WorkoutSetPersonalBest::Reps,
+        ],
+    ),
+    (ExerciseLot::Reps, &[WorkoutSetPersonalBest::Reps]),
+];
+
 #[derive(Debug, Clone, Serialize, Enum, Copy, Deserialize, FromJsonQueryResult, Eq, PartialEq)]
 #[serde(rename_all = "snake_case")]
 pub enum ExerciseCategory {
@@ -141,7 +159,7 @@ pub struct ExerciseListItem {
 impl GraphqlRepresentation for ExerciseListItem {
     async fn graphql_representation(
         self,
-        file_storage_service: &Arc<FileStorageService>,
+        file_storage_service: &FileStorageService,
     ) -> GraphqlResult<Self> {
         let mut converted_exercise = self.clone();
         if let Some(img) = self.attributes.internal_images.first() {
@@ -440,6 +458,7 @@ pub enum UserUnitSystem {
     PartialEq,
     SimpleObject,
     Schematic,
+    Default,
 )]
 #[serde(rename_all = "snake_case")]
 pub struct WorkoutInformation {
@@ -476,6 +495,7 @@ pub struct WorkoutSummaryExercise {
     FromJsonQueryResult,
     Eq,
     PartialEq,
+    Default,
     SimpleObject,
     Schematic,
 )]
diff --git a/crates/models/media/src/lib.rs b/crates/models/media/src/lib.rs
index bd5b918cdc..5d29d22939 100644
--- a/crates/models/media/src/lib.rs
+++ b/crates/models/media/src/lib.rs
@@ -1,4 +1,4 @@
-use std::{collections::HashSet, fmt, sync::Arc};
+use std::{collections::HashSet, fmt};
 
 use async_graphql::{Enum, InputObject, InputType, OneofObject, SimpleObject, Union};
 use boilermates::boilermates;
@@ -609,7 +609,7 @@ pub struct ImportOrExportItemRating {
 
 /// Details about a specific media item that needs to be imported or exported.
 #[skip_serializing_none]
-#[derive(Debug, Serialize, Deserialize, Clone, Schematic)]
+#[derive(Debug, Serialize, Deserialize, Clone, Schematic, Default)]
 #[serde(rename_all = "snake_case")]
 pub struct ImportOrExportMediaItem {
     /// An string to help identify it in the original source.
@@ -717,7 +717,7 @@ pub struct MetadataVideo {
 
 pub async fn first_metadata_image_as_url(
     value: &Option<Vec<MetadataImage>>,
-    file_storage_service: &Arc<FileStorageService>,
+    file_storage_service: &FileStorageService,
 ) -> Option<String> {
     if let Some(images) = value {
         if let Some(i) = images.first().cloned() {
@@ -732,7 +732,7 @@ pub async fn first_metadata_image_as_url(
 
 pub async fn metadata_images_as_urls(
     value: &Option<Vec<MetadataImage>>,
-    file_storage_service: &Arc<FileStorageService>,
+    file_storage_service: &FileStorageService,
 ) -> Vec<String> {
     let mut images = vec![];
     if let Some(imgs) = value {
@@ -1044,30 +1044,6 @@ pub struct DeployImportJobInput {
     pub jellyfin: Option<DeployUrlAndKeyAndUsernameImportInput>,
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize, Default)]
-pub struct IntegrationMediaSeen {
-    pub identifier: String,
-    pub lot: MediaLot,
-    #[serde(default)]
-    pub source: MediaSource,
-    pub progress: Decimal,
-    pub show_season_number: Option<i32>,
-    pub show_episode_number: Option<i32>,
-    pub podcast_episode_number: Option<i32>,
-    pub anime_episode_number: Option<i32>,
-    pub manga_chapter_number: Option<Decimal>,
-    pub manga_volume_number: Option<i32>,
-    pub provider_watched_on: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize, Default)]
-pub struct IntegrationMediaCollection {
-    pub identifier: String,
-    pub lot: MediaLot,
-    pub source: MediaSource,
-    pub collection: String,
-}
-
 #[derive(Debug, Serialize, Deserialize, InputObject, Clone)]
 pub struct CreateCustomMetadataInput {
     pub title: String,
@@ -1660,3 +1636,9 @@ pub struct DailyUserActivityItem {
     pub total_count: i64,
     pub total_duration: i64,
 }
+
+#[derive(Debug, Ord, PartialEq, Eq, PartialOrd, Clone, Hash)]
+pub struct CommitCache {
+    pub id: String,
+    pub lot: EntityLot,
+}
diff --git a/crates/models/user/src/lib.rs b/crates/models/user/src/lib.rs
index 84f05be2b2..7335e50f4b 100644
--- a/crates/models/user/src/lib.rs
+++ b/crates/models/user/src/lib.rs
@@ -256,8 +256,8 @@ pub struct UserFitnessPreferences {
 )]
 #[strum(ascii_case_insensitive, serialize_all = "SCREAMING_SNAKE_CASE")]
 pub enum GridPacking {
-    #[default]
     Normal,
+    #[default]
     Dense,
 }
 
diff --git a/crates/providers/src/tmdb.rs b/crates/providers/src/tmdb.rs
index d2b15e5168..3aae9098e8 100644
--- a/crates/providers/src/tmdb.rs
+++ b/crates/providers/src/tmdb.rs
@@ -2,13 +2,13 @@ use std::{
     collections::{HashMap, HashSet},
     fs,
     path::PathBuf,
+    sync::Arc,
 };
 
 use anyhow::{anyhow, Result};
 use application_utils::{get_base_http_client, get_current_date};
 use async_trait::async_trait;
 use chrono::NaiveDate;
-use chrono_tz::Tz;
 use common_models::{IdObject, NamedObject, SearchDetails, StoredUrl};
 use common_utils::{
     convert_date_to_year, convert_string_to_date, SHOW_SPECIAL_SEASON_NAMES, TEMP_DIR,
@@ -170,7 +170,7 @@ pub struct TmdbService {
     client: Client,
     language: String,
     settings: Settings,
-    timezone: Tz,
+    timezone: Arc<chrono_tz::Tz>,
 }
 
 impl TmdbService {
@@ -388,12 +388,12 @@ pub struct NonMediaTmdbService {
 }
 
 impl NonMediaTmdbService {
-    pub async fn new(config: &config::TmdbConfig, timezone: chrono_tz::Tz) -> Self {
-        let (client, settings) = get_client_config(&config.access_token).await;
+    pub async fn new(access_token: &str, language: String, timezone: Arc<chrono_tz::Tz>) -> Self {
+        let (client, settings) = get_client_config(access_token).await;
         Self {
             base: TmdbService {
                 client,
-                language: config.locale.clone(),
+                language,
                 settings,
                 timezone,
             },
@@ -617,7 +617,7 @@ pub struct TmdbMovieService {
 impl TmdbMovieService {
     pub async fn new(
         config: &config::TmdbConfig,
-        timezone: chrono_tz::Tz,
+        timezone: Arc<chrono_tz::Tz>,
         _page_limit: i32,
     ) -> Self {
         let (client, settings) = get_client_config(&config.access_token).await;
@@ -975,7 +975,7 @@ pub struct TmdbShowService {
 impl TmdbShowService {
     pub async fn new(
         config: &config::TmdbConfig,
-        timezone: chrono_tz::Tz,
+        timezone: Arc<chrono_tz::Tz>,
         _page_limit: i32,
     ) -> Self {
         let (client, settings) = get_client_config(&config.access_token).await;
diff --git a/crates/resolvers/router/Cargo.toml b/crates/resolvers/router/Cargo.toml
index 59bf11a038..ac32d7343e 100644
--- a/crates/resolvers/router/Cargo.toml
+++ b/crates/resolvers/router/Cargo.toml
@@ -9,9 +9,9 @@ async-graphql = { workspace = true }
 axum = { workspace = true }
 common-utils = { path = "../../utils/common" }
 config = { path = "../../config" }
+integration-service = { path = "../../services/integration" }
 nanoid = { workspace = true }
 serde_json = { workspace = true }
-miscellaneous-service = { path = "../../services/miscellaneous" }
 tracing = { workspace = true }
 
 [package.metadata.cargo-machete]
diff --git a/crates/resolvers/router/src/lib.rs b/crates/resolvers/router/src/lib.rs
index a95f19d75d..1a59eff0f3 100644
--- a/crates/resolvers/router/src/lib.rs
+++ b/crates/resolvers/router/src/lib.rs
@@ -9,7 +9,7 @@ use axum::{
     Extension, Json,
 };
 use common_utils::{ryot_log, TEMP_DIR};
-use miscellaneous_service::MiscellaneousService;
+use integration_service::IntegrationService;
 use nanoid::nanoid;
 use serde_json::json;
 
@@ -49,10 +49,10 @@ pub async fn upload_file(
 
 pub async fn integration_webhook(
     Path(integration_slug): Path<String>,
-    Extension(media_service): Extension<Arc<MiscellaneousService>>,
+    Extension(integration_service): Extension<Arc<IntegrationService>>,
     payload: String,
 ) -> std::result::Result<(StatusCode, String), StatusCode> {
-    let response = media_service
+    let response = integration_service
         .process_integration_webhook(integration_slug, payload)
         .await
         .map_err(|e| {
diff --git a/crates/services/collection/Cargo.toml b/crates/services/collection/Cargo.toml
index e576e1bbf8..704ea15057 100644
--- a/crates/services/collection/Cargo.toml
+++ b/crates/services/collection/Cargo.toml
@@ -4,11 +4,8 @@ version = "0.1.0"
 edition = "2021"
 
 [dependencies]
-apalis = { workspace = true }
 async-graphql = { workspace = true }
-background = { path = "../../background" }
 common-models = { path = "../../models/common" }
-config = { path = "../../config" }
 database-models = { path = "../../models/database" }
 database-utils = { path = "../../utils/database" }
 dependent-models = { path = "../../models/dependent" }
@@ -17,3 +14,4 @@ media-models = { path = "../../models/media" }
 migrations = { path = "../../migrations" }
 sea-orm = { workspace = true }
 sea-query = { workspace = true }
+supporting-service = { path = "../supporting" }
diff --git a/crates/services/collection/src/lib.rs b/crates/services/collection/src/lib.rs
index 68a4e251e3..ab77128d90 100644
--- a/crates/services/collection/src/lib.rs
+++ b/crates/services/collection/src/lib.rs
@@ -1,8 +1,6 @@
 use std::sync::Arc;
 
-use apalis::prelude::MemoryStorage;
 use async_graphql::{Error, Result};
-use background::CoreApplicationJob;
 use common_models::{
     ChangeCollectionToEntityInput, DefaultCollection, SearchDetails, StringIdObject,
 };
@@ -29,32 +27,15 @@ use migrations::{
     AliasedMetadataGroup, AliasedPerson, AliasedUser, AliasedUserToEntity,
 };
 use sea_orm::{
-    ColumnTrait, DatabaseConnection, EntityTrait, ItemsAndPagesNumber, Iterable, JoinType,
-    ModelTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, QueryTrait,
+    ColumnTrait, EntityTrait, ItemsAndPagesNumber, Iterable, JoinType, ModelTrait, PaginatorTrait,
+    QueryFilter, QueryOrder, QuerySelect, QueryTrait,
 };
 use sea_query::{
     extension::postgres::PgExpr, Alias, Condition, Expr, Func, PgFunc, Query, SimpleExpr,
 };
+use supporting_service::SupportingService;
 
-pub struct CollectionService {
-    db: DatabaseConnection,
-    config: Arc<config::AppConfig>,
-    perform_core_application_job: MemoryStorage<CoreApplicationJob>,
-}
-
-impl CollectionService {
-    pub fn new(
-        db: &DatabaseConnection,
-        config: Arc<config::AppConfig>,
-        perform_core_application_job: &MemoryStorage<CoreApplicationJob>,
-    ) -> Self {
-        Self {
-            config,
-            db: db.clone(),
-            perform_core_application_job: perform_core_application_job.clone(),
-        }
-    }
-}
+pub struct CollectionService(pub Arc<SupportingService>);
 
 impl CollectionService {
     pub async fn user_collections_list(
@@ -142,7 +123,7 @@ impl CollectionService {
             .left_join(UserToEntity)
             .filter(user_to_entity::Column::UserId.eq(user_id))
             .into_model::<CollectionItem>()
-            .all(&self.db)
+            .all(&self.0.db)
             .await
             .unwrap();
         Ok(collections)
@@ -157,7 +138,7 @@ impl CollectionService {
         let filter = input.filter.unwrap_or_default();
         let page: u64 = search.page.unwrap_or(1).try_into().unwrap();
         let maybe_collection = Collection::find_by_id(input.collection_id.clone())
-            .one(&self.db)
+            .one(&self.0.db)
             .await
             .unwrap();
         let collection = match maybe_collection {
@@ -167,7 +148,7 @@ impl CollectionService {
 
         let take = input
             .take
-            .unwrap_or_else(|| self.config.frontend.page_size.try_into().unwrap());
+            .unwrap_or_else(|| self.0.config.frontend.page_size.try_into().unwrap());
         let results = if take != 0 {
             let paginator = CollectionToEntity::find()
                 .left_join(Metadata)
@@ -246,7 +227,7 @@ impl CollectionService {
                     },
                     sort.order.into(),
                 )
-                .paginate(&self.db, take);
+                .paginate(&self.0.db, take);
             let mut items = vec![];
             let ItemsAndPagesNumber {
                 number_of_items,
@@ -275,9 +256,13 @@ impl CollectionService {
                 items: vec![],
             }
         };
-        let user = collection.find_related(User).one(&self.db).await?.unwrap();
+        let user = collection
+            .find_related(User)
+            .one(&self.0.db)
+            .await?
+            .unwrap();
         let reviews = item_reviews(
-            &self.db,
+            &self.0.db,
             &collection.user_id,
             &input.collection_id,
             EntityLot::Collection,
@@ -297,7 +282,7 @@ impl CollectionService {
         user_id: &String,
         input: CreateOrUpdateCollectionInput,
     ) -> Result<StringIdObject> {
-        create_or_update_collection(&self.db, user_id, input).await
+        create_or_update_collection(&self.0.db, user_id, input).await
     }
 
     pub async fn delete_collection(&self, user_id: String, name: &str) -> Result<bool> {
@@ -307,10 +292,13 @@ impl CollectionService {
         let collection = Collection::find()
             .filter(collection::Column::Name.eq(name))
             .filter(collection::Column::UserId.eq(user_id.to_owned()))
-            .one(&self.db)
+            .one(&self.0.db)
             .await?;
         let resp = if let Some(c) = collection {
-            Collection::delete_by_id(c.id).exec(&self.db).await.is_ok()
+            Collection::delete_by_id(c.id)
+                .exec(&self.0.db)
+                .await
+                .is_ok()
         } else {
             false
         };
@@ -322,7 +310,7 @@ impl CollectionService {
         user_id: &String,
         input: ChangeCollectionToEntityInput,
     ) -> Result<bool> {
-        add_entity_to_collection(&self.db, user_id, input, &self.perform_core_application_job).await
+        add_entity_to_collection(user_id, input, &self.0).await
     }
 
     pub async fn remove_entity_from_collection(
@@ -330,6 +318,6 @@ impl CollectionService {
         user_id: &String,
         input: ChangeCollectionToEntityInput,
     ) -> Result<StringIdObject> {
-        remove_entity_from_collection(&self.db, user_id, input).await
+        remove_entity_from_collection(&self.0.db, user_id, input).await
     }
 }
diff --git a/crates/services/exporter/Cargo.toml b/crates/services/exporter/Cargo.toml
index 63d34a77f5..cf67ce8c4e 100644
--- a/crates/services/exporter/Cargo.toml
+++ b/crates/services/exporter/Cargo.toml
@@ -8,14 +8,12 @@ apalis = { workspace = true }
 async-graphql = { workspace = true }
 background = { path = "../../background" }
 chrono = { workspace = true }
-config = { path = "../../config" }
 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" }
-file-storage-service = { path = "../file-storage" }
 fitness-models = { path = "../../models/fitness" }
 media-models = { path = "../../models/media" }
 mime_guess = "=2.0.5"
@@ -23,5 +21,6 @@ nanoid = { workspace = true }
 reqwest = { workspace = true }
 sea-orm = { workspace = true }
 struson = { version = "=0.5.0", features = ["serde"] }
+supporting-service = { path = "../supporting" }
 tokio = { workspace = true }
 tokio-util = { version = "=0.7.11", features = ["codec"] }
diff --git a/crates/services/exporter/src/lib.rs b/crates/services/exporter/src/lib.rs
index 9e8c4e0156..4fefd3cfd2 100644
--- a/crates/services/exporter/src/lib.rs
+++ b/crates/services/exporter/src/lib.rs
@@ -1,6 +1,6 @@
 use std::{collections::HashMap, fs::File as StdFile, path::PathBuf, sync::Arc};
 
-use apalis::prelude::{MemoryStorage, MessageQueue};
+use apalis::prelude::MessageQueue;
 use async_graphql::{Error, Result};
 use background::ApplicationJob;
 use chrono::{DateTime, Utc};
@@ -18,7 +18,6 @@ use database_utils::{
 };
 use dependent_models::{ImportOrExportWorkoutItem, ImportOrExportWorkoutTemplateItem};
 use enums::EntityLot;
-use file_storage_service::FileStorageService;
 use fitness_models::UserMeasurementsListInput;
 use media_models::{
     ImportOrExportExerciseItem, ImportOrExportItemRating, ImportOrExportItemReview,
@@ -31,10 +30,11 @@ use reqwest::{
     Body, Client,
 };
 use sea_orm::{
-    strum::Display, ColumnTrait, DatabaseConnection, EntityTrait, EnumIter, Iterable, ModelTrait,
-    QueryFilter, QueryOrder, QuerySelect,
+    strum::Display, ColumnTrait, EntityTrait, EnumIter, Iterable, ModelTrait, QueryFilter,
+    QueryOrder, QuerySelect,
 };
 use struson::writer::{JsonStreamWriter, JsonWriter};
+use supporting_service::SupportingService;
 use tokio::fs::File;
 use tokio_util::codec::{BytesCodec, FramedRead};
 
@@ -50,30 +50,12 @@ enum ExportItem {
     WorkoutTemplates,
 }
 
-pub struct ExporterService {
-    db: DatabaseConnection,
-    config: Arc<config::AppConfig>,
-    file_storage_service: Arc<FileStorageService>,
-    perform_application_job: MemoryStorage<ApplicationJob>,
-}
+pub struct ExporterService(pub Arc<SupportingService>);
 
 impl ExporterService {
-    pub fn new(
-        db: &DatabaseConnection,
-        config: Arc<config::AppConfig>,
-        perform_application_job: &MemoryStorage<ApplicationJob>,
-        file_storage_service: Arc<FileStorageService>,
-    ) -> Self {
-        Self {
-            db: db.clone(),
-            config,
-            file_storage_service,
-            perform_application_job: perform_application_job.clone(),
-        }
-    }
-
     pub async fn deploy_export_job(&self, user_id: String) -> Result<bool> {
-        self.perform_application_job
+        self.0
+            .perform_application_job
             .clone()
             .enqueue(ApplicationJob::PerformExport(user_id))
             .await
@@ -82,20 +64,23 @@ impl ExporterService {
     }
 
     pub async fn user_exports(&self, user_id: String) -> Result<Vec<ExportJob>> {
-        if !self.config.file_storage.is_enabled() {
+        if !self.0.config.file_storage.is_enabled() {
             return Ok(vec![]);
         }
         let mut resp = vec![];
         let objects = self
+            .0
             .file_storage_service
             .list_objects_at_prefix(format!("exports/{}", user_id))
             .await;
         for (size, object_key) in objects {
             let url = self
+                .0
                 .file_storage_service
                 .get_presigned_url(object_key.clone())
                 .await;
             let metadata = self
+                .0
                 .file_storage_service
                 .get_object_metadata(object_key)
                 .await;
@@ -118,7 +103,7 @@ impl ExporterService {
     }
 
     pub async fn perform_export(&self, user_id: String) -> Result<bool> {
-        if !self.config.file_storage.is_enabled() {
+        if !self.0.config.file_storage.is_enabled() {
             return Err(Error::new(
                 "File storage needs to be enabled to perform an export.",
             ));
@@ -148,6 +133,7 @@ impl ExporterService {
         writer.finish_document().unwrap();
         let ended_at = Utc::now();
         let (_key, url) = self
+            .0
             .file_storage_service
             .get_presigned_put_url(
                 export_path
@@ -191,20 +177,20 @@ impl ExporterService {
         let related_metadata = UserToEntity::find()
             .filter(user_to_entity::Column::UserId.eq(user_id))
             .filter(user_to_entity::Column::MetadataId.is_not_null())
-            .all(&self.db)
+            .all(&self.0.db)
             .await
             .unwrap();
         for rm in related_metadata.iter() {
             let m = rm
                 .find_related(Metadata)
-                .one(&self.db)
+                .one(&self.0.db)
                 .await
                 .unwrap()
                 .unwrap();
             let seen_history = m
                 .find_related(Seen)
                 .filter(seen::Column::UserId.eq(user_id))
-                .all(&self.db)
+                .all(&self.0.db)
                 .await
                 .unwrap();
             let seen_history = seen_history
@@ -233,16 +219,17 @@ impl ExporterService {
                     }
                 })
                 .collect();
-            let reviews = item_reviews(&self.db, user_id, &m.id, EntityLot::Metadata, false)
+            let reviews = item_reviews(&self.0.db, user_id, &m.id, EntityLot::Metadata, false)
                 .await?
                 .into_iter()
                 .map(|r| self.get_review_export_item(r))
                 .collect();
-            let collections = entity_in_collections(&self.db, user_id, &m.id, EntityLot::Metadata)
-                .await?
-                .into_iter()
-                .map(|c| c.name)
-                .collect();
+            let collections =
+                entity_in_collections(&self.0.db, user_id, &m.id, EntityLot::Metadata)
+                    .await?
+                    .into_iter()
+                    .map(|c| c.name)
+                    .collect();
             let exp = ImportOrExportMediaItem {
                 source_id: m.title,
                 lot: m.lot,
@@ -265,23 +252,23 @@ impl ExporterService {
         let related_metadata = UserToEntity::find()
             .filter(user_to_entity::Column::UserId.eq(user_id))
             .filter(user_to_entity::Column::MetadataGroupId.is_not_null())
-            .all(&self.db)
+            .all(&self.0.db)
             .await
             .unwrap();
         for rm in related_metadata.iter() {
             let m = rm
                 .find_related(MetadataGroup)
-                .one(&self.db)
+                .one(&self.0.db)
                 .await
                 .unwrap()
                 .unwrap();
-            let reviews = item_reviews(&self.db, user_id, &m.id, EntityLot::MetadataGroup, false)
+            let reviews = item_reviews(&self.0.db, user_id, &m.id, EntityLot::MetadataGroup, false)
                 .await?
                 .into_iter()
                 .map(|r| self.get_review_export_item(r))
                 .collect();
             let collections =
-                entity_in_collections(&self.db, user_id, &m.id, EntityLot::MetadataGroup)
+                entity_in_collections(&self.0.db, user_id, &m.id, EntityLot::MetadataGroup)
                     .await?
                     .into_iter()
                     .map(|c| c.name)
@@ -307,22 +294,22 @@ impl ExporterService {
         let related_people = UserToEntity::find()
             .filter(user_to_entity::Column::UserId.eq(user_id))
             .filter(user_to_entity::Column::PersonId.is_not_null())
-            .all(&self.db)
+            .all(&self.0.db)
             .await
             .unwrap();
         for rm in related_people.iter() {
             let p = rm
                 .find_related(Person)
-                .one(&self.db)
+                .one(&self.0.db)
                 .await
                 .unwrap()
                 .unwrap();
-            let reviews = item_reviews(&self.db, user_id, &p.id, EntityLot::Person, false)
+            let reviews = item_reviews(&self.0.db, user_id, &p.id, EntityLot::Person, false)
                 .await?
                 .into_iter()
                 .map(|r| self.get_review_export_item(r))
                 .collect();
-            let collections = entity_in_collections(&self.db, user_id, &p.id, EntityLot::Person)
+            let collections = entity_in_collections(&self.0.db, user_id, &p.id, EntityLot::Person)
                 .await?
                 .into_iter()
                 .map(|c| c.name)
@@ -351,11 +338,10 @@ impl ExporterService {
             .filter(workout::Column::UserId.eq(user_id))
             .order_by_desc(workout::Column::EndTime)
             .into_tuple::<String>()
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         for workout_id in workout_ids {
-            let details =
-                workout_details(&self.db, &self.file_storage_service, user_id, workout_id).await?;
+            let details = workout_details(user_id, workout_id, &self.0).await?;
             let exp = ImportOrExportWorkoutItem {
                 details: details.details,
                 collections: details.collections.into_iter().map(|c| c.name).collect(),
@@ -371,7 +357,7 @@ impl ExporterService {
         writer: &mut JsonStreamWriter<StdFile>,
     ) -> Result<()> {
         let measurements = user_measurements_list(
-            &self.db,
+            &self.0.db,
             user_id,
             UserMeasurementsListInput {
                 start_time: None,
@@ -393,26 +379,27 @@ impl ExporterService {
         let exercises = UserToEntity::find()
             .filter(user_to_entity::Column::UserId.eq(user_id))
             .filter(user_to_entity::Column::ExerciseId.is_not_null())
-            .all(&self.db)
+            .all(&self.0.db)
             .await
             .unwrap();
         for rm in exercises.iter() {
             let e = rm
                 .find_related(Exercise)
-                .one(&self.db)
+                .one(&self.0.db)
                 .await
                 .unwrap()
                 .unwrap();
-            let reviews = item_reviews(&self.db, user_id, &e.id, EntityLot::Exercise, false)
+            let reviews = item_reviews(&self.0.db, user_id, &e.id, EntityLot::Exercise, false)
                 .await?
                 .into_iter()
                 .map(|r| self.get_review_export_item(r))
                 .collect();
-            let collections = entity_in_collections(&self.db, user_id, &e.id, EntityLot::Exercise)
-                .await?
-                .into_iter()
-                .map(|c| c.name)
-                .collect();
+            let collections =
+                entity_in_collections(&self.0.db, user_id, &e.id, EntityLot::Exercise)
+                    .await?
+                    .into_iter()
+                    .map(|c| c.name)
+                    .collect();
             let exp = ImportOrExportExerciseItem {
                 name: e.id,
                 collections,
@@ -434,10 +421,11 @@ impl ExporterService {
             .filter(workout_template::Column::UserId.eq(user_id))
             .order_by_desc(workout_template::Column::CreatedOn)
             .into_tuple::<String>()
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         for workout_template_id in workout_template_ids {
-            let details = workout_template_details(&self.db, user_id, workout_template_id).await?;
+            let details =
+                workout_template_details(&self.0.db, user_id, workout_template_id).await?;
             let exp = ImportOrExportWorkoutTemplateItem {
                 details: details.details,
                 collections: details.collections.into_iter().map(|c| c.name).collect(),
diff --git a/crates/services/fitness/Cargo.toml b/crates/services/fitness/Cargo.toml
index 461b437f33..09b40d1986 100644
--- a/crates/services/fitness/Cargo.toml
+++ b/crates/services/fitness/Cargo.toml
@@ -11,24 +11,22 @@ async-graphql = { workspace = true }
 background = { path = "../../background" }
 common-models = { path = "../../models/common" }
 common-utils = { path = "../../utils/common" }
-config = { path = "../../config" }
 const-str = { workspace = true }
 database-models = { path = "../../models/database" }
 database-utils = { path = "../../utils/database" }
 dependent-models = { path = "../../models/dependent" }
+dependent-utils = { path = "../../utils/dependent" }
 enums = { path = "../../enums" }
-file-storage-service = { path = "../../services/file-storage" }
 fitness-models = { path = "../../models/fitness" }
 itertools = { workspace = true }
 migrations = { path = "../../migrations" }
 nanoid = { workspace = true }
 reqwest = { workspace = true }
-rust_decimal = { workspace = true }
-rust_decimal_macros = { workspace = true }
 sea-orm = { workspace = true }
 sea-query = { workspace = true }
 slug = { workspace = true }
+supporting-service = { path = "../supporting" }
 tracing = { workspace = true }
 
 [package.metadata.cargo-machete]
-ignored = ["rust_decimal", "tracing"]
+ignored = ["tracing"]
diff --git a/crates/services/fitness/src/lib.rs b/crates/services/fitness/src/lib.rs
index ea7fcc7dcd..69a87e544d 100644
--- a/crates/services/fitness/src/lib.rs
+++ b/crates/services/fitness/src/lib.rs
@@ -1,9 +1,9 @@
 use std::sync::Arc;
 
-use apalis::prelude::{MemoryStorage, MessageQueue};
+use apalis::prelude::MessageQueue;
 use application_utils::GraphqlRepresentation;
 use async_graphql::{Error, Result};
-use background::{ApplicationJob, CoreApplicationJob};
+use background::ApplicationJob;
 use common_models::{
     ChangeCollectionToEntityInput, DefaultCollection, SearchDetails, SearchInput, StoredUrl,
 };
@@ -24,82 +24,40 @@ use dependent_models::{
     SearchResults, UpdateCustomExerciseInput, UserExerciseDetails, UserWorkoutDetails,
     UserWorkoutTemplateDetails,
 };
+use dependent_utils::{
+    create_or_update_workout, create_user_measurement, db_workout_to_workout_input,
+};
 use enums::{
     EntityLot, ExerciseEquipment, ExerciseForce, ExerciseLevel, ExerciseLot, ExerciseMechanic,
     ExerciseMuscle, ExerciseSource, Visibility,
 };
-use file_storage_service::FileStorageService;
 use fitness_models::{
     ExerciseAttributes, ExerciseCategory, ExerciseFilters, ExerciseListItem, ExerciseParameters,
     ExerciseParametersLotMapping, ExerciseSortBy, ExercisesListInput, GithubExercise,
-    GithubExerciseAttributes, ProcessedExercise, UpdateUserWorkoutInput, UserExerciseInput,
-    UserMeasurementsListInput, UserWorkoutInput, UserWorkoutSetRecord, WorkoutInformation,
-    WorkoutSetPersonalBest, WorkoutSetRecord, WorkoutSummary, WorkoutSummaryExercise,
+    GithubExerciseAttributes, ProcessedExercise, UpdateUserWorkoutInput, UserMeasurementsListInput,
+    UserWorkoutInput, WorkoutInformation, WorkoutSetRecord, WorkoutSummary, WorkoutSummaryExercise,
+    LOT_MAPPINGS,
 };
 use itertools::Itertools;
 use migrations::AliasedExercise;
 use nanoid::nanoid;
 use sea_orm::{
-    prelude::DateTimeUtc, ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection,
-    EntityTrait, Iterable, ModelTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect,
-    QueryTrait, RelationTrait,
+    prelude::DateTimeUtc, ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, Iterable,
+    ModelTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, QueryTrait, RelationTrait,
 };
 use sea_query::{extension::postgres::PgExpr, Alias, Condition, Expr, Func, JoinType, OnConflict};
 use slug::slugify;
 
-use logic::{create_or_update_workout, delete_existing_workout};
+use logic::delete_existing_workout;
+use supporting_service::SupportingService;
 
 mod logic;
 
 const EXERCISE_DB_URL: &str = "https://raw.githubusercontent.com/yuhonas/free-exercise-db/main";
 const JSON_URL: &str = const_str::concat!(EXERCISE_DB_URL, "/dist/exercises.json");
 const IMAGES_PREFIX_URL: &str = const_str::concat!(EXERCISE_DB_URL, "/exercises");
-const LOT_MAPPINGS: &[(ExerciseLot, &[WorkoutSetPersonalBest])] = &[
-    (ExerciseLot::Duration, &[WorkoutSetPersonalBest::Time]),
-    (
-        ExerciseLot::DistanceAndDuration,
-        &[WorkoutSetPersonalBest::Pace, WorkoutSetPersonalBest::Time],
-    ),
-    (
-        ExerciseLot::RepsAndWeight,
-        &[
-            WorkoutSetPersonalBest::Weight,
-            WorkoutSetPersonalBest::OneRm,
-            WorkoutSetPersonalBest::Volume,
-            WorkoutSetPersonalBest::Reps,
-        ],
-    ),
-    (ExerciseLot::Reps, &[WorkoutSetPersonalBest::Reps]),
-];
 
-pub struct ExerciseService {
-    is_pro: bool,
-    db: DatabaseConnection,
-    config: Arc<config::AppConfig>,
-    file_storage_service: Arc<FileStorageService>,
-    perform_application_job: MemoryStorage<ApplicationJob>,
-    perform_core_application_job: MemoryStorage<CoreApplicationJob>,
-}
-
-impl ExerciseService {
-    pub fn new(
-        is_pro: bool,
-        db: &DatabaseConnection,
-        config: Arc<config::AppConfig>,
-        file_storage_service: Arc<FileStorageService>,
-        perform_application_job: &MemoryStorage<ApplicationJob>,
-        perform_core_application_job: &MemoryStorage<CoreApplicationJob>,
-    ) -> Self {
-        Self {
-            config,
-            is_pro,
-            db: db.clone(),
-            file_storage_service,
-            perform_application_job: perform_application_job.clone(),
-            perform_core_application_job: perform_core_application_job.clone(),
-        }
-    }
-}
+pub struct ExerciseService(pub Arc<SupportingService>);
 
 impl ExerciseService {
     pub async fn user_workout_templates_list(
@@ -114,11 +72,14 @@ impl ExerciseService {
                 query.filter(Expr::col(workout_template::Column::Name).ilike(ilike_sql(&v)))
             })
             .order_by_desc(workout_template::Column::CreatedOn);
-        let total = query.clone().count(&self.db).await?;
+        let total = query.clone().count(&self.0.db).await?;
         let total: i32 = total.try_into().unwrap();
-        let data = query.paginate(&self.db, self.config.frontend.page_size.try_into().unwrap());
+        let data = query.paginate(
+            &self.0.db,
+            self.0.config.frontend.page_size.try_into().unwrap(),
+        );
         let items = data.fetch_page((page - 1).try_into().unwrap()).await?;
-        let next_page = if total - (page * self.config.frontend.page_size) > 0 {
+        let next_page = if total - (page * self.0.config.frontend.page_size) > 0 {
             Some(page + 1)
         } else {
             None
@@ -134,7 +95,7 @@ impl ExerciseService {
         user_id: String,
         workout_template_id: String,
     ) -> Result<UserWorkoutTemplateDetails> {
-        workout_template_details(&self.db, &user_id, workout_template_id).await
+        workout_template_details(&self.0.db, &user_id, workout_template_id).await
     }
 
     pub async fn create_or_update_user_workout_template(
@@ -142,7 +103,7 @@ impl ExerciseService {
         user_id: String,
         input: UserWorkoutInput,
     ) -> Result<String> {
-        pro_instance_guard(self.is_pro).await?;
+        pro_instance_guard(self.0.is_pro).await?;
         let mut summary = WorkoutSummary {
             total: None,
             exercises: vec![],
@@ -208,7 +169,7 @@ impl ExerciseService {
                     ])
                     .to_owned(),
             )
-            .exec_with_returning(&self.db)
+            .exec_with_returning(&self.0.db)
             .await?;
         Ok(template.id)
     }
@@ -218,13 +179,13 @@ impl ExerciseService {
         user_id: String,
         workout_template_id: String,
     ) -> Result<bool> {
-        pro_instance_guard(self.is_pro).await?;
+        pro_instance_guard(self.0.is_pro).await?;
         if let Some(wkt) = WorkoutTemplate::find_by_id(workout_template_id)
             .filter(workout_template::Column::UserId.eq(&user_id))
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
         {
-            wkt.delete(&self.db).await?;
+            wkt.delete(&self.0.db).await?;
             Ok(true)
         } else {
             Err(Error::new("Workout template does not exist for user"))
@@ -232,7 +193,7 @@ impl ExerciseService {
     }
 
     pub async fn exercise_parameters(&self) -> Result<ExerciseParameters> {
-        let download_required = Exercise::find().count(&self.db).await? == 0;
+        let download_required = Exercise::find().count(&self.0.db).await? == 0;
         Ok(ExerciseParameters {
             filters: ExerciseFilters {
                 lot: ExerciseLot::iter().collect_vec(),
@@ -278,10 +239,12 @@ impl ExerciseService {
     }
 
     pub async fn exercise_details(&self, exercise_id: String) -> Result<exercise::Model> {
-        let maybe_exercise = Exercise::find_by_id(exercise_id).one(&self.db).await?;
+        let maybe_exercise = Exercise::find_by_id(exercise_id).one(&self.0.db).await?;
         match maybe_exercise {
             None => Err(Error::new("Exercise with the given ID could not be found.")),
-            Some(e) => Ok(e.graphql_representation(&self.file_storage_service).await?),
+            Some(e) => Ok(e
+                .graphql_representation(&self.0.file_storage_service)
+                .await?),
         }
     }
 
@@ -290,7 +253,7 @@ impl ExerciseService {
         user_id: &String,
         workout_id: String,
     ) -> Result<UserWorkoutDetails> {
-        workout_details(&self.db, &self.file_storage_service, user_id, workout_id).await
+        workout_details(user_id, workout_id, &self.0).await
     }
 
     pub async fn user_exercise_details(
@@ -299,9 +262,15 @@ impl ExerciseService {
         exercise_id: String,
     ) -> Result<UserExerciseDetails> {
         let collections =
-            entity_in_collections(&self.db, &user_id, &exercise_id, EntityLot::Exercise).await?;
-        let reviews =
-            item_reviews(&self.db, &user_id, &exercise_id, EntityLot::Exercise, true).await?;
+            entity_in_collections(&self.0.db, &user_id, &exercise_id, EntityLot::Exercise).await?;
+        let reviews = item_reviews(
+            &self.0.db,
+            &user_id,
+            &exercise_id,
+            EntityLot::Exercise,
+            true,
+        )
+        .await?;
         let mut resp = UserExerciseDetails {
             details: None,
             history: None,
@@ -311,7 +280,7 @@ impl ExerciseService {
         if let Some(association) = UserToEntity::find()
             .filter(user_to_entity::Column::UserId.eq(user_id))
             .filter(user_to_entity::Column::ExerciseId.eq(exercise_id))
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
         {
             let user_to_exercise_extra_information = association
@@ -336,11 +305,14 @@ impl ExerciseService {
                 query.filter(Expr::col(workout::Column::Name).ilike(ilike_sql(&v)))
             })
             .order_by_desc(workout::Column::EndTime);
-        let total = query.clone().count(&self.db).await?;
+        let total = query.clone().count(&self.0.db).await?;
         let total: i32 = total.try_into().unwrap();
-        let data = query.paginate(&self.db, self.config.frontend.page_size.try_into().unwrap());
+        let data = query.paginate(
+            &self.0.db,
+            self.0.config.frontend.page_size.try_into().unwrap(),
+        );
         let items = data.fetch_page((page - 1).try_into().unwrap()).await?;
-        let next_page = if total - (page * self.config.frontend.page_size) > 0 {
+        let next_page = if total - (page * self.0.config.frontend.page_size) > 0 {
             Some(page + 1)
         } else {
             None
@@ -438,23 +410,24 @@ impl ExerciseService {
             )
             .order_by_desc(order_by_col)
             .order_by_asc(exercise::Column::Id);
-        let total = query.clone().count(&self.db).await?;
+        let total = query.clone().count(&self.0.db).await?;
         let total: i32 = total.try_into().unwrap();
-        let data = query
-            .into_model::<ExerciseListItem>()
-            .paginate(&self.db, self.config.frontend.page_size.try_into().unwrap());
+        let data = query.into_model::<ExerciseListItem>().paginate(
+            &self.0.db,
+            self.0.config.frontend.page_size.try_into().unwrap(),
+        );
         let mut items = vec![];
         for ex in data
             .fetch_page((input.search.page.unwrap() - 1).try_into().unwrap())
             .await?
         {
             let gql_repr = ex
-                .graphql_representation(&self.file_storage_service)
+                .graphql_representation(&self.0.file_storage_service)
                 .await?;
             items.push(gql_repr);
         }
         let next_page =
-            if total - ((input.search.page.unwrap()) * self.config.frontend.page_size) > 0 {
+            if total - ((input.search.page.unwrap()) * self.0.config.frontend.page_size) > 0 {
                 Some(input.search.page.unwrap() + 1)
             } else {
                 None
@@ -468,7 +441,8 @@ impl ExerciseService {
     pub async fn deploy_update_exercise_library_job(&self) -> Result<bool> {
         let exercises = self.get_all_exercises_from_dataset().await?;
         for exercise in exercises {
-            self.perform_application_job
+            self.0
+                .perform_application_job
                 .clone()
                 .enqueue(ApplicationJob::UpdateGithubExerciseJob(exercise))
                 .await
@@ -493,7 +467,7 @@ impl ExerciseService {
         if let Some(e) = Exercise::find()
             .filter(exercise::Column::Identifier.eq(&ex.identifier))
             .filter(exercise::Column::Source.eq(ExerciseSource::Github))
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
         {
             ryot_log!(
@@ -504,7 +478,7 @@ impl ExerciseService {
             let mut db_ex: exercise::ActiveModel = e.into();
             db_ex.attributes = ActiveValue::Set(attributes);
             db_ex.muscles = ActiveValue::Set(muscles);
-            db_ex.update(&self.db).await?;
+            db_ex.update(&self.0.db).await?;
         } else {
             let lot = match ex.attributes.category {
                 ExerciseCategory::Cardio => ExerciseLot::DistanceAndDuration,
@@ -529,7 +503,7 @@ impl ExerciseService {
                 mechanic: ActiveValue::Set(ex.attributes.mechanic),
                 created_by_user_id: ActiveValue::Set(None),
             };
-            let created_exercise = db_exercise.insert(&self.db).await?;
+            let created_exercise = db_exercise.insert(&self.0.db).await?;
             ryot_log!(
                 debug,
                 "Created new exercise with id: {}",
@@ -544,18 +518,15 @@ impl ExerciseService {
         user_id: &String,
         input: UserMeasurementsListInput,
     ) -> Result<Vec<user_measurement::Model>> {
-        user_measurements_list(&self.db, user_id, input).await
+        user_measurements_list(&self.0.db, user_id, input).await
     }
 
     pub async fn create_user_measurement(
         &self,
         user_id: &String,
-        mut input: user_measurement::Model,
+        input: user_measurement::Model,
     ) -> Result<DateTimeUtc> {
-        input.user_id = user_id.to_owned();
-        let um: user_measurement::ActiveModel = input.into();
-        let um = um.insert(&self.db).await?;
-        Ok(um.timestamp)
+        create_user_measurement(user_id, input, &self.0.db).await
     }
 
     pub async fn delete_user_measurement(
@@ -564,10 +535,10 @@ impl ExerciseService {
         timestamp: DateTimeUtc,
     ) -> Result<bool> {
         if let Some(m) = UserMeasurement::find_by_id((timestamp, user_id))
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
         {
-            m.delete(&self.db).await?;
+            m.delete(&self.0.db).await?;
             Ok(true)
         } else {
             Ok(false)
@@ -579,9 +550,7 @@ impl ExerciseService {
         user_id: &String,
         input: UserWorkoutInput,
     ) -> Result<String> {
-        let identifier =
-            create_or_update_workout(input, user_id, &self.db, &self.perform_application_job)
-                .await?;
+        let identifier = create_or_update_workout(input, user_id, &self.0).await?;
         Ok(identifier)
     }
 
@@ -593,7 +562,7 @@ impl ExerciseService {
         if let Some(wkt) = Workout::find()
             .filter(workout::Column::UserId.eq(&user_id))
             .filter(workout::Column::Id.eq(input.id))
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
         {
             let mut new_wkt: workout::ActiveModel = wkt.into();
@@ -604,8 +573,8 @@ impl ExerciseService {
                 new_wkt.end_time = ActiveValue::Set(d);
             }
             if new_wkt.is_changed() {
-                new_wkt.update(&self.db).await?;
-                deploy_job_to_re_evaluate_user_workouts(&self.perform_application_job, &user_id)
+                new_wkt.update(&self.0.db).await?;
+                deploy_job_to_re_evaluate_user_workouts(&user_id, &self.0.perform_application_job)
                     .await;
                 Ok(true)
             } else {
@@ -636,17 +605,16 @@ impl ExerciseService {
         let input: exercise::ActiveModel = input.into();
         let exercise = match Exercise::find_by_id(exercise_id)
             .filter(exercise::Column::Source.eq(ExerciseSource::Custom))
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
         {
-            None => input.insert(&self.db).await?,
+            None => input.insert(&self.0.db).await?,
             Some(_) => {
                 let input = input.reset_all();
-                input.update(&self.db).await?
+                input.update(&self.0.db).await?
             }
         };
         add_entity_to_collection(
-            &self.db,
             &user_id.clone(),
             ChangeCollectionToEntityInput {
                 creator_user_id: user_id,
@@ -655,7 +623,7 @@ impl ExerciseService {
                 entity_lot: EntityLot::Exercise,
                 ..Default::default()
             },
-            &self.perform_core_application_job,
+            &self.0,
         )
         .await?;
         Ok(exercise.id)
@@ -664,10 +632,10 @@ impl ExerciseService {
     pub async fn delete_user_workout(&self, user_id: String, workout_id: String) -> Result<bool> {
         if let Some(wkt) = Workout::find_by_id(workout_id)
             .filter(workout::Column::UserId.eq(&user_id))
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
         {
-            delete_existing_workout(wkt, &self.db, user_id).await?;
+            delete_existing_workout(wkt, &self.0.db, user_id).await?;
             Ok(true)
         } else {
             Err(Error::new("Workout does not exist for user"))
@@ -678,17 +646,17 @@ impl ExerciseService {
         UserToEntity::delete_many()
             .filter(user_to_entity::Column::UserId.eq(&user_id))
             .filter(user_to_entity::Column::ExerciseId.is_not_null())
-            .exec(&self.db)
+            .exec(&self.0.db)
             .await?;
         let workouts = Workout::find()
             .filter(workout::Column::UserId.eq(&user_id))
             .order_by_asc(workout::Column::EndTime)
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         let total = workouts.len();
         for (idx, workout) in workouts.into_iter().enumerate() {
-            workout.clone().delete(&self.db).await?;
-            let workout_input = self.db_workout_to_workout_input(workout);
+            workout.clone().delete(&self.0.db).await?;
+            let workout_input = db_workout_to_workout_input(workout);
             self.create_or_update_user_workout(&user_id, workout_input)
                 .await?;
             ryot_log!(debug, "Re-evaluated workout: {}/{}", idx + 1, total);
@@ -696,44 +664,6 @@ impl ExerciseService {
         Ok(())
     }
 
-    pub fn db_workout_to_workout_input(&self, user_workout: workout::Model) -> UserWorkoutInput {
-        UserWorkoutInput {
-            name: user_workout.name,
-            create_workout_id: Some(user_workout.id),
-            update_workout_id: None,
-            default_rest_timer: None,
-            end_time: user_workout.end_time,
-            update_workout_template_id: None,
-            start_time: user_workout.start_time,
-            template_id: user_workout.template_id,
-            assets: user_workout.information.assets,
-            repeated_from: user_workout.repeated_from,
-            comment: user_workout.information.comment,
-            exercises: user_workout
-                .information
-                .exercises
-                .into_iter()
-                .map(|e| UserExerciseInput {
-                    exercise_id: e.name,
-                    sets: e
-                        .sets
-                        .into_iter()
-                        .map(|s| UserWorkoutSetRecord {
-                            lot: s.lot,
-                            note: s.note,
-                            statistic: s.statistic,
-                            confirmed_at: s.confirmed_at,
-                        })
-                        .collect(),
-                    notes: e.notes,
-                    rest_time: e.rest_time,
-                    assets: e.assets,
-                    superset_with: e.superset_with,
-                })
-                .collect(),
-        }
-    }
-
     pub async fn update_custom_exercise(
         &self,
         user_id: String,
@@ -742,10 +672,10 @@ impl ExerciseService {
         let entities = UserToEntity::find()
             .filter(user_to_entity::Column::UserId.eq(&user_id))
             .filter(user_to_entity::Column::ExerciseId.eq(input.old_name.clone()))
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         let old_exercise = Exercise::find_by_id(input.old_name.clone())
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
             .unwrap();
         if input.should_delete.unwrap_or_default() {
@@ -761,12 +691,12 @@ impl ExerciseService {
                     ));
                 }
             }
-            old_exercise.delete(&self.db).await?;
+            old_exercise.delete(&self.0.db).await?;
             return Ok(true);
         }
         if input.old_name != input.update.id {
             if Exercise::find_by_id(input.update.id.clone())
-                .one(&self.db)
+                .one(&self.0.db)
                 .await?
                 .is_some()
             {
@@ -775,12 +705,12 @@ impl ExerciseService {
             Exercise::update_many()
                 .col_expr(exercise::Column::Id, Expr::value(input.update.id.clone()))
                 .filter(exercise::Column::Id.eq(input.old_name.clone()))
-                .exec(&self.db)
+                .exec(&self.0.db)
                 .await?;
             for entity in entities {
                 for workout in entity.exercise_extra_information.unwrap().history {
                     let db_workout = Workout::find_by_id(workout.workout_id)
-                        .one(&self.db)
+                        .one(&self.0.db)
                         .await?
                         .unwrap();
                     let mut summary = db_workout.summary.clone();
@@ -790,14 +720,14 @@ impl ExerciseService {
                     let mut db_workout: workout::ActiveModel = db_workout.into();
                     db_workout.summary = ActiveValue::Set(summary);
                     db_workout.information = ActiveValue::Set(information);
-                    db_workout.update(&self.db).await?;
+                    db_workout.update(&self.0.db).await?;
                 }
             }
         }
         for image in old_exercise.attributes.internal_images {
             match image {
                 StoredUrl::S3(key) => {
-                    self.file_storage_service.delete_object(key).await;
+                    self.0.file_storage_service.delete_object(key).await;
                 }
                 _ => continue,
             }
diff --git a/crates/services/fitness/src/logic.rs b/crates/services/fitness/src/logic.rs
index 0ee1a72d4a..9ffd19d77d 100644
--- a/crates/services/fitness/src/logic.rs
+++ b/crates/services/fitness/src/logic.rs
@@ -1,332 +1,10 @@
-use anyhow::{bail, Result};
-use apalis::prelude::MemoryStorage;
-use background::ApplicationJob;
-use common_utils::ryot_log;
-use database_models::{
-    prelude::{Exercise, UserToEntity, Workout},
-    user_to_entity, workout,
-};
-use database_utils::deploy_job_to_re_evaluate_user_workouts;
-use fitness_models::{
-    ExerciseBestSetRecord, ProcessedExercise, UserToExerciseBestSetExtraInformation,
-    UserToExerciseExtraInformation, UserToExerciseHistoryExtraInformation, UserWorkoutInput,
-    WorkoutInformation, WorkoutOrExerciseTotals, WorkoutSetPersonalBest, WorkoutSetRecord,
-    WorkoutSetTotals, WorkoutSummary, WorkoutSummaryExercise,
-};
-use nanoid::nanoid;
-use rust_decimal_macros::dec;
+use anyhow::Result;
+use database_models::{prelude::UserToEntity, user_to_entity, workout};
 use sea_orm::{
     ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait,
     QueryFilter,
 };
 
-use super::LOT_MAPPINGS;
-
-fn get_best_set_index(records: &[WorkoutSetRecord]) -> Option<usize> {
-    records
-        .iter()
-        .enumerate()
-        .max_by_key(|(_, record)| {
-            record.statistic.duration.unwrap_or(dec!(0))
-                + record.statistic.distance.unwrap_or(dec!(0))
-                + record.statistic.reps.unwrap_or(dec!(0))
-                + record.statistic.weight.unwrap_or(dec!(0))
-        })
-        .map(|(index, _)| index)
-}
-
-fn get_index_of_highest_pb(
-    records: &[WorkoutSetRecord],
-    pb_type: &WorkoutSetPersonalBest,
-) -> Option<usize> {
-    let record = records.iter().reduce(|record1, record2| {
-        let pb1 = record1.get_personal_best(pb_type);
-        let pb2 = record2.get_personal_best(pb_type);
-        match (pb1, pb2) {
-            (Some(pb1), Some(pb2)) => {
-                if pb1 > pb2 {
-                    record1
-                } else {
-                    record2
-                }
-            }
-            _ => record1,
-        }
-    });
-    record.and_then(|r| records.iter().position(|l| l == r))
-}
-
-/// Create or update a workout in the database and also update user and exercise associations.
-pub async fn create_or_update_workout(
-    input: UserWorkoutInput,
-    user_id: &String,
-    db: &DatabaseConnection,
-    perform_application_job: &MemoryStorage<ApplicationJob>,
-) -> Result<String> {
-    let end_time = input.end_time;
-    let mut input = input;
-    let (new_workout_id, to_update_workout) = match &input.update_workout_id {
-        Some(id) => (
-            id.to_owned(),
-            // DEV: Unwrap to make sure we error out early if the workout to edit does not exist
-            Some(Workout::find_by_id(id).one(db).await?.unwrap()),
-        ),
-        None => (
-            input
-                .create_workout_id
-                .unwrap_or_else(|| format!("wor_{}", nanoid!(12))),
-            None,
-        ),
-    };
-    ryot_log!(debug, "Creating new workout with id = {}", new_workout_id);
-    let mut exercises = vec![];
-    let mut workout_totals = vec![];
-    if input.exercises.is_empty() {
-        bail!("This workout has no associated exercises")
-    }
-    let mut first_set_of_exercise_confirmed_at = input
-        .exercises
-        .first()
-        .unwrap()
-        .sets
-        .first()
-        .unwrap()
-        .confirmed_at;
-    for (exercise_idx, ex) in input.exercises.iter_mut().enumerate() {
-        if ex.sets.is_empty() {
-            bail!("This exercise has no associated sets")
-        }
-        let db_ex = match Exercise::find_by_id(ex.exercise_id.clone()).one(db).await? {
-            None => {
-                ryot_log!(error, "Exercise with id = {} not found", ex.exercise_id);
-                continue;
-            }
-            Some(e) => e,
-        };
-        let mut sets = vec![];
-        let mut total = WorkoutOrExerciseTotals::default();
-        let association = UserToEntity::find()
-            .filter(user_to_entity::Column::UserId.eq(user_id))
-            .filter(user_to_entity::Column::ExerciseId.eq(ex.exercise_id.clone()))
-            .one(db)
-            .await
-            .ok()
-            .flatten();
-        let history_item = UserToExerciseHistoryExtraInformation {
-            best_set: None,
-            idx: exercise_idx,
-            workout_id: new_workout_id.clone(),
-            workout_end_on: end_time,
-        };
-        let association = match association {
-            None => {
-                let user_to_ex = user_to_entity::ActiveModel {
-                    user_id: ActiveValue::Set(user_id.clone()),
-                    exercise_id: ActiveValue::Set(Some(ex.exercise_id.clone())),
-                    exercise_extra_information: ActiveValue::Set(Some(
-                        UserToExerciseExtraInformation {
-                            history: vec![history_item],
-                            lifetime_stats: WorkoutOrExerciseTotals::default(),
-                            personal_bests: vec![],
-                        },
-                    )),
-                    created_on: ActiveValue::Set(
-                        first_set_of_exercise_confirmed_at.unwrap_or(end_time),
-                    ),
-                    exercise_num_times_interacted: ActiveValue::Set(Some(1)),
-                    ..Default::default()
-                };
-                user_to_ex.insert(db).await.unwrap()
-            }
-            Some(e) => {
-                let last_updated_on = e.last_updated_on;
-                let mut extra_info = e.exercise_extra_information.clone().unwrap_or_default();
-                extra_info.history.insert(0, history_item);
-                let mut to_update: user_to_entity::ActiveModel = e.into();
-                to_update.exercise_num_times_interacted =
-                    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));
-                to_update.update(db).await?
-            }
-        };
-        if let Some(d) = ex.rest_time {
-            total.rest_time += d * (ex.sets.len() - 1) as u16;
-        }
-        ex.sets
-            .sort_unstable_by_key(|s| s.confirmed_at.unwrap_or_default());
-        for set in ex.sets.iter_mut() {
-            let mut actual_rest_time = None;
-            if exercise_idx != 0
-                && set.confirmed_at.is_some()
-                && first_set_of_exercise_confirmed_at.is_some()
-            {
-                actual_rest_time = Some(
-                    (set.confirmed_at.unwrap() - first_set_of_exercise_confirmed_at.unwrap())
-                        .num_seconds(),
-                );
-            }
-            first_set_of_exercise_confirmed_at = set.confirmed_at;
-            set.remove_invalids(&db_ex.lot);
-            if let Some(r) = set.statistic.reps {
-                total.reps += r;
-                if let Some(w) = set.statistic.weight {
-                    total.weight += w * r;
-                }
-            }
-            if let Some(d) = set.statistic.duration {
-                total.duration += d;
-            }
-            if let Some(d) = set.statistic.distance {
-                total.distance += d;
-            }
-            let mut totals = WorkoutSetTotals::default();
-            if let (Some(we), Some(re)) = (&set.statistic.weight, &set.statistic.reps) {
-                totals.weight = Some(we * re);
-            }
-            let mut value = WorkoutSetRecord {
-                lot: set.lot,
-                actual_rest_time,
-                totals: Some(totals),
-                note: set.note.clone(),
-                personal_bests: Some(vec![]),
-                confirmed_at: set.confirmed_at,
-                statistic: set.statistic.clone(),
-            };
-            value.statistic.one_rm = value.calculate_one_rm();
-            value.statistic.pace = value.calculate_pace();
-            value.statistic.volume = value.calculate_volume();
-            sets.push(value);
-        }
-        let mut personal_bests = association
-            .exercise_extra_information
-            .clone()
-            .unwrap_or_default()
-            .personal_bests;
-        let types_of_prs = LOT_MAPPINGS
-            .iter()
-            .find(|lm| lm.0 == db_ex.lot)
-            .map(|lm| lm.1)
-            .unwrap();
-        for best_type in types_of_prs.iter() {
-            let set_idx = get_index_of_highest_pb(&sets, best_type).unwrap();
-            let possible_record = personal_bests
-                .iter()
-                .find(|pb| pb.lot == *best_type)
-                .and_then(|record| record.sets.first());
-            let set = sets.get_mut(set_idx).unwrap();
-            if let Some(r) = possible_record {
-                if let Some(workout) = Workout::find_by_id(r.workout_id.clone()).one(db).await? {
-                    let workout_set =
-                        workout.information.exercises[r.exercise_idx].sets[r.set_idx].clone();
-                    if set.get_personal_best(best_type) > workout_set.get_personal_best(best_type) {
-                        if let Some(ref mut set_personal_bests) = set.personal_bests {
-                            set_personal_bests.push(*best_type);
-                        }
-                        total.personal_bests_achieved += 1;
-                    }
-                }
-            } else {
-                if let Some(ref mut set_personal_bests) = set.personal_bests {
-                    set_personal_bests.push(*best_type);
-                }
-                total.personal_bests_achieved += 1;
-            }
-        }
-        workout_totals.push(total.clone());
-        for (set_idx, set) in sets.iter().enumerate() {
-            if let Some(set_personal_bests) = &set.personal_bests {
-                for best in set_personal_bests.iter() {
-                    let to_insert_record = ExerciseBestSetRecord {
-                        workout_id: new_workout_id.clone(),
-                        exercise_idx,
-                        set_idx,
-                    };
-                    if let Some(record) = personal_bests.iter_mut().find(|pb| pb.lot == *best) {
-                        let mut data = record.sets.clone();
-                        data.insert(0, to_insert_record);
-                        record.sets = data;
-                    } else {
-                        personal_bests.push(UserToExerciseBestSetExtraInformation {
-                            lot: *best,
-                            sets: vec![to_insert_record],
-                        });
-                    }
-                }
-            }
-        }
-        let best_set = get_best_set_index(&sets).and_then(|i| sets.get(i).cloned());
-        let mut association_extra_information = association
-            .exercise_extra_information
-            .clone()
-            .unwrap_or_default();
-        association_extra_information.history[0].best_set = best_set.clone();
-        let mut association: user_to_entity::ActiveModel = association.into();
-        association_extra_information.lifetime_stats += total.clone();
-        association_extra_information.personal_bests = personal_bests;
-        association.exercise_extra_information =
-            ActiveValue::Set(Some(association_extra_information));
-        association.update(db).await?;
-        exercises.push((
-            best_set,
-            db_ex.lot,
-            ProcessedExercise {
-                sets,
-                lot: db_ex.lot,
-                name: db_ex.id,
-                total: Some(total),
-                notes: ex.notes.clone(),
-                rest_time: ex.rest_time,
-                assets: ex.assets.clone(),
-                superset_with: ex.superset_with.clone(),
-            },
-        ));
-    }
-    let summary_total = workout_totals.into_iter().sum();
-    let model = workout::Model {
-        end_time,
-        name: input.name,
-        user_id: user_id.clone(),
-        id: new_workout_id.clone(),
-        start_time: input.start_time,
-        repeated_from: input.repeated_from,
-        summary: WorkoutSummary {
-            total: Some(summary_total),
-            exercises: exercises
-                .clone()
-                .into_iter()
-                .map(|(best_set, lot, e)| WorkoutSummaryExercise {
-                    best_set,
-                    lot: Some(lot),
-                    id: e.name.clone(),
-                    num_sets: e.sets.len(),
-                })
-                .collect(),
-        },
-        information: WorkoutInformation {
-            comment: input.comment,
-            assets: input.assets,
-            exercises: exercises.into_iter().map(|(_, _, ex)| ex).collect(),
-        },
-        template_id: input.template_id,
-        duration: 0,
-    };
-    let mut insert: workout::ActiveModel = model.into();
-    insert.duration = ActiveValue::NotSet;
-    if let Some(old_workout) = to_update_workout.clone() {
-        insert.end_time = ActiveValue::Set(old_workout.end_time);
-        insert.start_time = ActiveValue::Set(old_workout.start_time);
-        insert.repeated_from = ActiveValue::Set(old_workout.repeated_from.clone());
-        old_workout.delete(db).await?;
-    }
-    let data = insert.insert(db).await?;
-    if to_update_workout.is_some() {
-        deploy_job_to_re_evaluate_user_workouts(perform_application_job, user_id).await;
-    }
-    Ok(data.id)
-}
-
 // DEV: For exercises, reduce count, remove from history if present. We will not
 // re-calculate exercise associations totals or change personal bests.
 pub async fn delete_existing_workout(
diff --git a/crates/services/importer/Cargo.toml b/crates/services/importer/Cargo.toml
index 68a85e62a3..a03ca9cb91 100644
--- a/crates/services/importer/Cargo.toml
+++ b/crates/services/importer/Cargo.toml
@@ -19,16 +19,14 @@ env-utils = { path = "../../utils/env" }
 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" }
+dependent-utils = { path = "../../utils/dependent" }
 enums = { path = "../../enums" }
 fitness-models = { path = "../../models/fitness" }
-fitness-service = { path = "../../services/fitness" }
 flate2 = "=1.0.33"
 importer-models = { path = "../../models/importer" }
 itertools = { workspace = true }
 media-models = { path = "../../models/media" }
-miscellaneous-service = { path = "../../services/miscellaneous" }
 providers = { path = "../../providers" }
 regex = { workspace = true }
 reqwest = { workspace = true }
@@ -40,9 +38,9 @@ serde_json = { workspace = true }
 serde_with = { workspace = true }
 serde-xml-rs = "=0.6.0"
 specific-models = { path = "../../models/specific" }
+supporting-service = { path = "../supporting" }
 tracing = { workspace = true }
 traits = { path = "../../traits" }
-user-models = { path = "../../models/user" }
 
 [package.metadata.cargo-machete]
 ignored = ["tracing"]
diff --git a/crates/services/importer/src/audiobookshelf.rs b/crates/services/importer/src/audiobookshelf.rs
index 312b1221bb..2b4c91a042 100644
--- a/crates/services/importer/src/audiobookshelf.rs
+++ b/crates/services/importer/src/audiobookshelf.rs
@@ -167,13 +167,12 @@ where
                 identifier,
                 seen_history,
                 source_id: metadata.title,
-                collections: vec![],
-                reviews: vec![],
+                ..Default::default()
             })
         }
     }
     Ok(ImportResult {
-        media,
+        metadata: media,
         failed_items,
         ..Default::default()
     })
diff --git a/crates/services/importer/src/generic_json.rs b/crates/services/importer/src/generic_json.rs
index 8071729c92..31420cc2da 100644
--- a/crates/services/importer/src/generic_json.rs
+++ b/crates/services/importer/src/generic_json.rs
@@ -1,16 +1,12 @@
-use std::{fs, sync::Arc};
+use std::fs;
 
 use async_graphql::Result;
 use dependent_models::{CompleteExport, ImportResult};
 use enums::ImportSource;
-use fitness_service::ExerciseService;
 use itertools::Itertools;
 use media_models::DeployJsonImportInput;
 
-pub async fn import(
-    input: DeployJsonImportInput,
-    exercises_service: &Arc<ExerciseService>,
-) -> Result<ImportResult> {
+pub async fn import(input: DeployJsonImportInput) -> Result<ImportResult> {
     let export = fs::read_to_string(input.export)?;
     let complete_data = serde_json::from_str::<CompleteExport>(&export).unwrap();
 
@@ -26,19 +22,12 @@ pub async fn import(
         })
         .collect_vec();
 
-    let workouts = complete_data
-        .workouts
-        .unwrap_or_default()
-        .into_iter()
-        .map(|w| exercises_service.db_workout_to_workout_input(w.details))
-        .collect_vec();
-
     Ok(ImportResult {
-        media,
-        workouts,
-        media_groups: complete_data.media_group.unwrap_or_default(),
+        metadata: media,
         people: complete_data.people.unwrap_or_default(),
+        metadata_groups: complete_data.media_groups.unwrap_or_default(),
         measurements: complete_data.measurements.unwrap_or_default(),
+        application_workouts: complete_data.workouts.unwrap_or_default(),
         ..Default::default()
     })
 }
diff --git a/crates/services/importer/src/goodreads.rs b/crates/services/importer/src/goodreads.rs
index d20befd0c9..970e006e13 100644
--- a/crates/services/importer/src/goodreads.rs
+++ b/crates/services/importer/src/goodreads.rs
@@ -146,7 +146,7 @@ pub async fn import(
         }
     }
     Ok(ImportResult {
-        media,
+        metadata: media,
         failed_items,
         ..Default::default()
     })
diff --git a/crates/services/importer/src/igdb.rs b/crates/services/importer/src/igdb.rs
index 984a71c390..a1d9c44755 100644
--- a/crates/services/importer/src/igdb.rs
+++ b/crates/services/importer/src/igdb.rs
@@ -58,11 +58,11 @@ pub async fn import(input: DeployIgdbImportInput) -> Result<ImportResult> {
             identifier: record.id,
             seen_history: seen_history.clone(),
             collections: vec![collection.clone()],
-            reviews: vec![],
+            ..Default::default()
         });
     }
     Ok(ImportResult {
-        media,
+        metadata: media,
         failed_items,
         ..Default::default()
     })
diff --git a/crates/services/importer/src/imdb.rs b/crates/services/importer/src/imdb.rs
index e5fb155016..43d0e21b75 100644
--- a/crates/services/importer/src/imdb.rs
+++ b/crates/services/importer/src/imdb.rs
@@ -86,12 +86,11 @@ pub async fn import(
             source,
             source_id: record.id,
             collections: vec![DefaultCollection::Watchlist.to_string()],
-            reviews: vec![],
-            seen_history: vec![],
+            ..Default::default()
         });
     }
     Ok(ImportResult {
-        media,
+        metadata: media,
         failed_items,
         ..Default::default()
     })
diff --git a/crates/services/importer/src/jellyfin.rs b/crates/services/importer/src/jellyfin.rs
index ccb88e5590..86f0218223 100644
--- a/crates/services/importer/src/jellyfin.rs
+++ b/crates/services/importer/src/jellyfin.rs
@@ -178,8 +178,8 @@ pub async fn import(input: DeployUrlAndKeyAndUsernameImportInput) -> Result<Impo
                 source: MediaSource::Tmdb,
                 seen_history: vec![seen],
                 identifier: tmdb_id,
-                reviews: vec![],
                 collections,
+..Default::default()
             });
         } else {
             failed_items.push(ImportFailedItem {
@@ -209,7 +209,7 @@ pub async fn import(input: DeployUrlAndKeyAndUsernameImportInput) -> Result<Impo
     }
 
     Ok(ImportResult {
-        media,
+        metadata: media,
         failed_items,
         ..Default::default()
     })
diff --git a/crates/services/importer/src/lib.rs b/crates/services/importer/src/lib.rs
index ca35b44144..421b46193a 100644
--- a/crates/services/importer/src/lib.rs
+++ b/crates/services/importer/src/lib.rs
@@ -1,28 +1,22 @@
 use std::sync::Arc;
 
-use apalis::prelude::{MemoryStorage, MessageQueue};
+use apalis::prelude::MessageQueue;
 use async_graphql::Result;
-use background::{ApplicationJob, CoreApplicationJob};
+use background::ApplicationJob;
 use chrono::{DateTime, Duration, NaiveDateTime, Offset, TimeZone, Utc};
-use common_models::{BackgroundJob, ChangeCollectionToEntityInput};
+use common_models::BackgroundJob;
 use common_utils::ryot_log;
 use database_models::{import_report, prelude::ImportReport};
-use database_utils::{add_entity_to_collection, create_or_update_collection, user_by_id};
-use enums::{EntityLot, ImportSource};
-use fitness_service::ExerciseService;
-use importer_models::{ImportDetails, ImportFailStep, ImportFailedItem, ImportResultResponse};
-use media_models::{
-    CommitMediaInput, CommitPersonInput, CreateOrUpdateCollectionInput, CreateOrUpdateReviewInput,
-    DeployImportJobInput, ImportOrExportItemRating, ImportOrExportMediaItem, ProgressUpdateInput,
-};
-use miscellaneous_service::MiscellaneousService;
-use rust_decimal_macros::dec;
-use sea_orm::{
-    ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter,
-    QueryOrder,
+use dependent_utils::{
+    commit_metadata, deploy_background_job, get_isbn_service, get_tmdb_non_media_service,
+    process_import,
 };
+use enums::ImportSource;
+use importer_models::{ImportFailStep, ImportFailedItem, ImportResultResponse};
+use media_models::{DeployImportJobInput, ImportOrExportMediaItem};
+use sea_orm::{ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
+use supporting_service::SupportingService;
 use traits::TraceOk;
-use user_models::{UserPreferences, UserReviewScale};
 
 mod audiobookshelf;
 mod generic_json;
@@ -38,41 +32,17 @@ mod story_graph;
 mod strong_app;
 mod trakt;
 
-pub struct ImporterService {
-    db: DatabaseConnection,
-    perform_application_job: MemoryStorage<ApplicationJob>,
-    perform_core_application_job: MemoryStorage<CoreApplicationJob>,
-    media_service: Arc<MiscellaneousService>,
-    exercise_service: Arc<ExerciseService>,
-    timezone: Arc<chrono_tz::Tz>,
-}
+pub struct ImporterService(pub Arc<SupportingService>);
 
 impl ImporterService {
-    pub fn new(
-        db: &DatabaseConnection,
-        perform_application_job: &MemoryStorage<ApplicationJob>,
-        perform_core_application_job: &MemoryStorage<CoreApplicationJob>,
-        media_service: Arc<MiscellaneousService>,
-        exercise_service: Arc<ExerciseService>,
-        timezone: Arc<chrono_tz::Tz>,
-    ) -> Self {
-        Self {
-            media_service,
-            exercise_service,
-            timezone,
-            db: db.clone(),
-            perform_application_job: perform_application_job.clone(),
-            perform_core_application_job: perform_core_application_job.clone(),
-        }
-    }
-
     pub async fn deploy_import_job(
         &self,
         user_id: String,
         input: DeployImportJobInput,
     ) -> Result<bool> {
         let job = ApplicationJob::ImportFromExternalSource(user_id, Box::new(input));
-        self.perform_application_job
+        self.0
+            .perform_application_job
             .clone()
             .enqueue(job)
             .await
@@ -85,7 +55,7 @@ impl ImporterService {
         let reports = ImportReport::find()
             .filter(import_report::Column::UserId.eq(user_id))
             .order_by_desc(import_report::Column::StartedOn)
-            .all(&self.db)
+            .all(&self.0.db)
             .await
             .unwrap();
         Ok(reports)
@@ -97,10 +67,9 @@ impl ImporterService {
         input: Box<DeployImportJobInput>,
     ) -> Result<()> {
         let db_import_job = self.start_import_job(&user_id, input.source).await?;
-        let preferences = user_by_id(&self.db, &user_id).await?.preferences;
-        let mut import = match input.source {
+        let import = match input.source {
             ImportSource::StrongApp => {
-                strong_app::import(input.strong_app.unwrap(), self.timezone.clone())
+                strong_app::import(input.strong_app.unwrap(), &self.0.timezone)
                     .await
                     .unwrap()
             }
@@ -110,7 +79,7 @@ impl ImporterService {
             ImportSource::Mal => mal::import(input.mal.unwrap()).await.unwrap(),
             ImportSource::Goodreads => goodreads::import(
                 input.generic_csv.unwrap(),
-                &self.media_service.get_isbn_service().await.unwrap(),
+                &get_isbn_service(&self.0.config).await.unwrap(),
             )
             .await
             .unwrap(),
@@ -118,363 +87,43 @@ impl ImporterService {
             ImportSource::Movary => movary::import(input.movary.unwrap()).await.unwrap(),
             ImportSource::StoryGraph => story_graph::import(
                 input.generic_csv.unwrap(),
-                &self.media_service.get_isbn_service().await.unwrap(),
+                &get_isbn_service(&self.0.config).await.unwrap(),
             )
             .await
             .unwrap(),
             ImportSource::Audiobookshelf => audiobookshelf::import(
                 input.url_and_key.unwrap(),
-                &self.media_service.get_isbn_service().await.unwrap(),
-                |input| self.media_service.commit_metadata(input),
+                &get_isbn_service(&self.0.config).await.unwrap(),
+                |input| commit_metadata(input, &self.0),
             )
             .await
             .unwrap(),
             ImportSource::Igdb => igdb::import(input.igdb.unwrap()).await.unwrap(),
             ImportSource::Imdb => imdb::import(
                 input.generic_csv.unwrap(),
-                &self
-                    .media_service
-                    .get_tmdb_non_media_service()
-                    .await
-                    .unwrap(),
+                &get_tmdb_non_media_service(&self.0).await.unwrap(),
             )
             .await
             .unwrap(),
-            ImportSource::GenericJson => {
-                generic_json::import(input.generic_json.unwrap(), &self.exercise_service)
-                    .await
-                    .unwrap()
-            }
+            ImportSource::GenericJson => generic_json::import(input.generic_json.unwrap())
+                .await
+                .unwrap(),
             ImportSource::OpenScale => {
-                open_scale::import(input.generic_csv.unwrap(), self.timezone.clone())
+                open_scale::import(input.generic_csv.unwrap(), &self.0.timezone)
                     .await
                     .unwrap()
             }
             ImportSource::Jellyfin => jellyfin::import(input.jellyfin.unwrap()).await.unwrap(),
         };
-        for m in import.media.iter_mut() {
-            m.seen_history.sort_by(|a, b| {
-                a.ended_on
-                    .unwrap_or_default()
-                    .cmp(&b.ended_on.unwrap_or_default())
-            });
-        }
-        for col_details in import.collections.clone() {
-            create_or_update_collection(&self.db, &user_id, col_details).await?;
-        }
-        for (idx, item) in import.media.iter().enumerate() {
-            ryot_log!(
-                debug,
-                "Importing media with identifier = {iden}",
-                iden = &item.source_id
-            );
-            let rev_length = item.reviews.len();
-            let identifier = item.identifier.clone();
-            let data = self
-                .media_service
-                .commit_metadata(CommitMediaInput {
-                    identifier,
-                    lot: item.lot,
-                    source: item.source,
-                    force_update: Some(true),
-                })
-                .await;
-            let metadata = match data {
-                Ok(r) => r,
-                Err(e) => {
-                    ryot_log!(error, "{e:?}");
-                    import.failed_items.push(ImportFailedItem {
-                        lot: Some(item.lot),
-                        step: ImportFailStep::MediaDetailsFromProvider,
-                        identifier: item.source_id.to_owned(),
-                        error: Some(e.message),
-                    });
-                    continue;
-                }
-            };
-            for seen in item.seen_history.iter() {
-                let progress = if seen.progress.is_some() {
-                    seen.progress
-                } else {
-                    Some(dec!(100))
-                };
-                if let Err(e) = self
-                    .media_service
-                    .progress_update(
-                        ProgressUpdateInput {
-                            progress,
-                            change_state: None,
-                            date: seen.ended_on,
-                            metadata_id: metadata.id.clone(),
-                            show_season_number: seen.show_season_number,
-                            manga_volume_number: seen.manga_volume_number,
-                            show_episode_number: seen.show_episode_number,
-                            anime_episode_number: seen.anime_episode_number,
-                            manga_chapter_number: seen.manga_chapter_number,
-                            podcast_episode_number: seen.podcast_episode_number,
-                            provider_watched_on: seen.provider_watched_on.clone(),
-                        },
-                        &user_id,
-                        false,
-                    )
-                    .await
-                {
-                    import.failed_items.push(ImportFailedItem {
-                        lot: Some(item.lot),
-                        step: ImportFailStep::SeenHistoryConversion,
-                        identifier: item.source_id.to_owned(),
-                        error: Some(e.message),
-                    });
-                };
-            }
-            for review in item.reviews.iter() {
-                if let Some(input) = convert_review_into_input(
-                    review,
-                    &preferences,
-                    metadata.id.clone(),
-                    EntityLot::Metadata,
-                ) {
-                    if let Err(e) = self
-                        .media_service
-                        .create_or_update_review(&user_id, input)
-                        .await
-                    {
-                        import.failed_items.push(ImportFailedItem {
-                            lot: Some(item.lot),
-                            step: ImportFailStep::ReviewConversion,
-                            identifier: item.source_id.to_owned(),
-                            error: Some(e.message),
-                        });
-                    };
-                }
-            }
-            for col in item.collections.iter() {
-                create_or_update_collection(
-                    &self.db,
-                    &user_id,
-                    CreateOrUpdateCollectionInput {
-                        name: col.to_string(),
-                        ..Default::default()
-                    },
-                )
-                .await?;
-                add_entity_to_collection(
-                    &self.db,
-                    &user_id,
-                    ChangeCollectionToEntityInput {
-                        creator_user_id: user_id.clone(),
-                        collection_name: col.to_string(),
-                        entity_id: metadata.id.clone(),
-                        entity_lot: EntityLot::Metadata,
-                        ..Default::default()
-                    },
-                    &self.perform_core_application_job,
-                )
-                .await
-                .ok();
-            }
-            ryot_log!(
-                debug,
-                "Imported item: {idx}/{total}, lot: {lot}, history count: {hist}, review count: {rev}, collection count: {col}",
-                idx = idx + 1,
-                total = import.media.len(),
-                lot = item.lot,
-                hist = item.seen_history.len(),
-                rev = rev_length,
-                col = item.collections.len(),
-            );
-        }
-        for (idx, item) in import.media_groups.iter().enumerate() {
-            ryot_log!(
-                debug,
-                "Importing media group with identifier = {iden}",
-                iden = &item.title
-            );
-            let rev_length = item.reviews.len();
-            let data = self
-                .media_service
-                .commit_metadata_group_internal(&item.identifier, item.lot, item.source)
-                .await;
-            let metadata_group_id = match data {
-                Ok(r) => r.0,
-                Err(e) => {
-                    ryot_log!(error, "{e:?}");
-                    import.failed_items.push(ImportFailedItem {
-                        lot: Some(item.lot),
-                        step: ImportFailStep::MediaDetailsFromProvider,
-                        identifier: item.title.to_owned(),
-                        error: Some(e.message),
-                    });
-                    continue;
-                }
-            };
-            for review in item.reviews.iter() {
-                if let Some(input) = convert_review_into_input(
-                    review,
-                    &preferences,
-                    metadata_group_id.clone(),
-                    EntityLot::MetadataGroup,
-                ) {
-                    if let Err(e) = self
-                        .media_service
-                        .create_or_update_review(&user_id, input)
-                        .await
-                    {
-                        import.failed_items.push(ImportFailedItem {
-                            lot: Some(item.lot),
-                            step: ImportFailStep::ReviewConversion,
-                            identifier: item.title.to_owned(),
-                            error: Some(e.message),
-                        });
-                    };
-                }
-            }
-            for col in item.collections.iter() {
-                create_or_update_collection(
-                    &self.db,
-                    &user_id,
-                    CreateOrUpdateCollectionInput {
-                        name: col.to_string(),
-                        ..Default::default()
-                    },
-                )
-                .await?;
-                add_entity_to_collection(
-                    &self.db,
-                    &user_id,
-                    ChangeCollectionToEntityInput {
-                        creator_user_id: user_id.clone(),
-                        collection_name: col.to_string(),
-                        entity_id: metadata_group_id.clone(),
-                        entity_lot: EntityLot::MetadataGroup,
-                        ..Default::default()
-                    },
-                    &self.perform_core_application_job,
-                )
-                .await
-                .ok();
-            }
-            ryot_log!(
-                debug,
-                "Imported item: {idx}/{total}, lot: {lot}, review count: {rev}, collection count: {col}",
-                idx = idx + 1,
-                total = import.media.len(),
-                lot = item.lot,
-                rev = rev_length,
-                col = item.collections.len(),
-            );
-        }
-        for (idx, item) in import.people.iter().enumerate() {
-            let person = self
-                .media_service
-                .commit_person(CommitPersonInput {
-                    identifier: item.identifier.clone(),
-                    name: item.name.clone(),
-                    source: item.source,
-                    source_specifics: item.source_specifics.clone(),
-                })
-                .await?;
-            for review in item.reviews.iter() {
-                if let Some(input) = convert_review_into_input(
-                    review,
-                    &preferences,
-                    person.id.clone(),
-                    EntityLot::Person,
-                ) {
-                    if let Err(e) = self
-                        .media_service
-                        .create_or_update_review(&user_id, input)
-                        .await
-                    {
-                        import.failed_items.push(ImportFailedItem {
-                            lot: None,
-                            step: ImportFailStep::ReviewConversion,
-                            identifier: item.name.to_owned(),
-                            error: Some(e.message),
-                        });
-                    };
-                }
-            }
-            for col in item.collections.iter() {
-                create_or_update_collection(
-                    &self.db,
-                    &user_id,
-                    CreateOrUpdateCollectionInput {
-                        name: col.to_string(),
-                        ..Default::default()
-                    },
-                )
-                .await?;
-                add_entity_to_collection(
-                    &self.db,
-                    &user_id,
-                    ChangeCollectionToEntityInput {
-                        creator_user_id: user_id.clone(),
-                        collection_name: col.to_string(),
-                        entity_id: person.id.clone(),
-                        entity_lot: EntityLot::Person,
-                        ..Default::default()
-                    },
-                    &self.perform_core_application_job,
-                )
-                .await
-                .ok();
-            }
-            ryot_log!(
-                debug,
-                "Imported person: {idx}/{total}, name: {name}",
-                idx = idx + 1,
-                total = import.people.len(),
-                name = item.name,
-            );
-        }
-        for workout in import.workouts.clone() {
-            if let Err(err) = self
-                .exercise_service
-                .create_or_update_user_workout(&user_id, workout)
-                .await
-            {
-                import.failed_items.push(ImportFailedItem {
-                    lot: None,
-                    step: ImportFailStep::InputTransformation,
-                    identifier: "Exercise".to_string(),
-                    error: Some(err.message),
-                });
-            }
-        }
-        for measurement in import.measurements.clone() {
-            if let Err(err) = self
-                .exercise_service
-                .create_user_measurement(&user_id, measurement)
-                .await
-            {
-                import.failed_items.push(ImportFailedItem {
-                    lot: None,
-                    step: ImportFailStep::InputTransformation,
-                    identifier: "Measurement".to_string(),
-                    error: Some(err.message),
-                });
-            }
-        }
-
-        // TODO: Allow importing exercises
-
-        let details = ImportResultResponse {
-            import: ImportDetails {
-                total: import.collections.len()
-                    + import.media.len()
-                    + import.media_groups.len()
-                    + import.people.len()
-                    + import.workouts.len()
-                    + import.measurements.len(),
-            },
-            failed_items: import.failed_items,
-        };
+        let details = process_import(&user_id, import, &self.0).await?;
         self.finish_import_job(db_import_job, details).await?;
-        self.media_service
-            .deploy_background_job(&user_id, BackgroundJob::CalculateUserActivitiesAndSummary)
-            .await
-            .trace_ok();
+        deploy_background_job(
+            &user_id,
+            BackgroundJob::CalculateUserActivitiesAndSummary,
+            &self.0,
+        )
+        .await
+        .trace_ok();
         Ok(())
     }
 
@@ -488,7 +137,7 @@ impl ImporterService {
             source: ActiveValue::Set(source),
             ..Default::default()
         };
-        let model = model.insert(&self.db).await.unwrap();
+        let model = model.insert(&self.0.db).await.unwrap();
         ryot_log!(debug, "Started import job with id = {id}", id = model.id);
         Ok(model)
     }
@@ -502,50 +151,17 @@ impl ImporterService {
         model.finished_on = ActiveValue::Set(Some(Utc::now()));
         model.details = ActiveValue::Set(Some(details));
         model.was_success = ActiveValue::Set(Some(true));
-        let model = model.update(&self.db).await.unwrap();
+        let model = model.update(&self.0.db).await.unwrap();
         Ok(model)
     }
 }
 
-fn convert_review_into_input(
-    review: &ImportOrExportItemRating,
-    preferences: &UserPreferences,
-    entity_id: String,
-    entity_lot: EntityLot,
-) -> Option<CreateOrUpdateReviewInput> {
-    if review.review.is_none() && review.rating.is_none() {
-        ryot_log!(debug, "Skipping review since it has no content");
-        return None;
-    }
-    let rating = match preferences.general.review_scale {
-        UserReviewScale::OutOfFive => review.rating.map(|rating| rating / dec!(20)),
-        UserReviewScale::OutOfHundred | UserReviewScale::ThreePointSmiley => review.rating,
-    };
-    let text = review.review.clone().and_then(|r| r.text);
-    let is_spoiler = review.review.clone().map(|r| r.spoiler.unwrap_or(false));
-    let date = review.review.clone().map(|r| r.date);
-    Some(CreateOrUpdateReviewInput {
-        rating,
-        text,
-        is_spoiler,
-        visibility: review.review.clone().and_then(|r| r.visibility),
-        date: date.flatten(),
-        entity_id,
-        entity_lot,
-        show_season_number: review.show_season_number,
-        show_episode_number: review.show_episode_number,
-        podcast_episode_number: review.podcast_episode_number,
-        manga_chapter_number: review.manga_chapter_number,
-        ..Default::default()
-    })
-}
-
 pub mod utils {
     use super::*;
 
     pub fn get_date_time_with_offset(
         date_time: NaiveDateTime,
-        timezone: Arc<chrono_tz::Tz>,
+        timezone: &chrono_tz::Tz,
     ) -> DateTime<Utc> {
         let offset = timezone
             .offset_from_utc_datetime(&Utc::now().naive_utc())
diff --git a/crates/services/importer/src/mal.rs b/crates/services/importer/src/mal.rs
index 69d174f916..fd8f7ef96f 100644
--- a/crates/services/importer/src/mal.rs
+++ b/crates/services/importer/src/mal.rs
@@ -35,7 +35,7 @@ pub async fn import(input: DeployMalImportInput) -> Result<ImportResult> {
         media.push(convert_to_format(item, MediaLot::Manga));
     }
     Ok(ImportResult {
-        media,
+        metadata: media,
         ..Default::default()
     })
 }
@@ -94,7 +94,7 @@ fn convert_to_format(item: Item, lot: MediaLot) -> ImportOrExportMediaItem {
         seen_history,
         source_id: item.title.clone(),
         reviews: vec![review_item],
-        collections: vec![],
+        ..Default::default()
     }
 }
 
diff --git a/crates/services/importer/src/media_tracker.rs b/crates/services/importer/src/media_tracker.rs
index 85fd2c9c78..f80c17bad2 100644
--- a/crates/services/importer/src/media_tracker.rs
+++ b/crates/services/importer/src/media_tracker.rs
@@ -318,7 +318,7 @@ pub async fn import(input: DeployUrlAndKeyImportInput) -> Result<ImportResult> {
         final_data.push(item);
     }
     Ok(ImportResult {
-        media: final_data,
+        metadata: final_data,
         failed_items,
         collections,
         ..Default::default()
diff --git a/crates/services/importer/src/movary.rs b/crates/services/importer/src/movary.rs
index 375f1cb9b9..82f213e1bb 100644
--- a/crates/services/importer/src/movary.rs
+++ b/crates/services/importer/src/movary.rs
@@ -63,13 +63,12 @@ pub async fn import(input: DeployMovaryImportInput) -> Result<ImportResult> {
             lot,
             source,
             identifier: record.common.tmdb_id.to_string(),
-            seen_history: vec![],
             reviews: vec![ImportOrExportItemRating {
                 // DEV: Rates items out of 10
                 rating: Some(record.user_rating.saturating_mul(dec!(10))),
                 ..Default::default()
             }],
-            collections: vec![],
+            ..Default::default()
         })
     }
     let mut watchlist_reader = Reader::from_path(input.watchlist).unwrap();
@@ -91,9 +90,8 @@ pub async fn import(input: DeployMovaryImportInput) -> Result<ImportResult> {
             lot,
             source,
             identifier: record.tmdb_id.to_string(),
-            seen_history: vec![],
-            reviews: vec![],
             collections: vec![DefaultCollection::Watchlist.to_string()],
+            ..Default::default()
         })
     }
     let mut history_reader = Reader::from_path(input.history).unwrap();
@@ -153,12 +151,12 @@ pub async fn import(input: DeployMovaryImportInput) -> Result<ImportResult> {
                 identifier: record.common.tmdb_id.to_string(),
                 seen_history: vec![seen_item],
                 reviews,
-                collections: vec![],
+                ..Default::default()
             })
         }
     }
     Ok(ImportResult {
-        media,
+        metadata: media,
         failed_items,
         ..Default::default()
     })
diff --git a/crates/services/importer/src/open_scale.rs b/crates/services/importer/src/open_scale.rs
index 9cfbf665f5..521c561a45 100644
--- a/crates/services/importer/src/open_scale.rs
+++ b/crates/services/importer/src/open_scale.rs
@@ -1,5 +1,3 @@
-use std::sync::Arc;
-
 use async_graphql::Result;
 use chrono::NaiveDateTime;
 use csv::ReaderBuilder;
@@ -40,7 +38,7 @@ struct Record {
 
 pub async fn import(
     input: DeployGenericCsvImportInput,
-    timezone: Arc<chrono_tz::Tz>,
+    timezone: &chrono_tz::Tz,
 ) -> Result<ImportResult> {
     let mut measurements = vec![];
     let mut failed_items = vec![];
@@ -64,7 +62,7 @@ pub async fn import(
         };
         let ndt = NaiveDateTime::parse_from_str(&record.date_time, "%Y-%m-%d %H:%M")
             .expect("Failed to parse input string");
-        let timestamp = utils::get_date_time_with_offset(ndt, timezone.clone());
+        let timestamp = utils::get_date_time_with_offset(ndt, timezone);
         measurements.push(user_measurement::Model {
             timestamp,
             user_id: "".to_string(),
diff --git a/crates/services/importer/src/story_graph.rs b/crates/services/importer/src/story_graph.rs
index 8fba940ea6..faa279711a 100644
--- a/crates/services/importer/src/story_graph.rs
+++ b/crates/services/importer/src/story_graph.rs
@@ -144,7 +144,7 @@ pub async fn import(
         }
     }
     Ok(ImportResult {
-        media,
+        metadata: media,
         failed_items,
         ..Default::default()
     })
diff --git a/crates/services/importer/src/strong_app.rs b/crates/services/importer/src/strong_app.rs
index aae3a67c98..0cd9f503cf 100644
--- a/crates/services/importer/src/strong_app.rs
+++ b/crates/services/importer/src/strong_app.rs
@@ -1,4 +1,4 @@
-use std::{fs, sync::Arc};
+use std::fs;
 
 use async_graphql::Result;
 use chrono::{Duration, NaiveDateTime};
@@ -39,7 +39,7 @@ struct Entry {
 
 pub async fn import(
     input: DeployStrongAppImportInput,
-    timezone: Arc<chrono_tz::Tz>,
+    timezone: &chrono_tz::Tz,
 ) -> Result<ImportResult> {
     let file_string = fs::read_to_string(&input.export_path)?;
     // DEV: Delimiter is `;` on android and `,` on iOS, so we determine it by reading the first line
@@ -95,12 +95,12 @@ pub async fn import(
         }
         if next_entry.set_order <= entry.set_order {
             exercises.push(UserExerciseInput {
-                exercise_id: target_exercise.target_name.clone(),
                 sets,
                 notes,
                 assets: None,
                 rest_time: None,
                 superset_with: vec![],
+                exercise_id: target_exercise.target_name.clone(),
             });
             sets = vec![];
             notes = vec![];
@@ -108,7 +108,7 @@ pub async fn import(
         if next_entry.date != entry.date {
             let ndt = NaiveDateTime::parse_from_str(&entry.date, "%Y-%m-%d %H:%M:%S")
                 .expect("Failed to parse input string");
-            let ndt = utils::get_date_time_with_offset(ndt, timezone.clone());
+            let ndt = utils::get_date_time_with_offset(ndt, timezone);
             let re = Regex::new(r"^(\d+h)?\s?(\d+m)?$").unwrap();
             let workout_duration = if let Some(captures) = re.captures(&entry.workout_duration) {
                 let hours = captures.get(1).map_or(0, |m| {
@@ -122,17 +122,17 @@ pub async fn import(
                 Duration::try_seconds(0).unwrap()
             };
             workouts.push(UserWorkoutInput {
-                create_workout_id: None,
+                exercises,
+                assets: None,
+                start_time: ndt,
                 template_id: None,
                 repeated_from: None,
                 default_rest_timer: None,
+                create_workout_id: None,
+                update_workout_id: None,
                 name: entry.workout_name,
                 comment: entry.workout_notes,
-                start_time: ndt,
                 end_time: ndt + workout_duration,
-                exercises,
-                assets: None,
-                update_workout_id: None,
                 update_workout_template_id: None,
             });
             exercises = vec![];
diff --git a/crates/services/importer/src/trakt.rs b/crates/services/importer/src/trakt.rs
index 2926d0625b..0cba04e27e 100644
--- a/crates/services/importer/src/trakt.rs
+++ b/crates/services/importer/src/trakt.rs
@@ -232,7 +232,7 @@ pub async fn import(input: DeployTraktImportInput) -> Result<ImportResult> {
     }
     Ok(ImportResult {
         collections,
-        media,
+        metadata: media,
         failed_items,
         ..Default::default()
     })
@@ -257,9 +257,7 @@ fn process_item(i: &ListItemResponse) -> Result<ImportOrExportMediaItem, ImportF
             lot,
             identifier: identifier.to_string(),
             source: MediaSource::Tmdb,
-            seen_history: vec![],
-            reviews: vec![],
-            collections: vec![],
+            ..Default::default()
         }),
         None => Err(ImportFailedItem {
             lot: None,
diff --git a/crates/services/integration/Cargo.toml b/crates/services/integration/Cargo.toml
index e15188658b..6e2906c7fd 100644
--- a/crates/services/integration/Cargo.toml
+++ b/crates/services/integration/Cargo.toml
@@ -7,12 +7,16 @@ edition = "2021"
 anyhow = { workspace = true }
 application-utils = { path = "../../utils/application" }
 async-graphql = { workspace = true }
-common-utils = { path = "../../utils/common" }
+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" }
+dependent-utils = { path = "../../utils/dependent" }
 enums = { path = "../../enums" }
 eventsource-stream = "=0.2.3"
+itertools = { workspace = true }
 media-models = { path = "../../models/media" }
 providers = { path = "../../providers" }
 radarr-api-rs = "=3.0.1"
@@ -26,9 +30,8 @@ serde = { workspace = true }
 serde_json = { workspace = true }
 sonarr-api-rs = "=3.0.0"
 specific-models = { path = "../../models/specific" }
+supporting-service = { path = "../supporting" }
 tokio = { workspace = true }
 tracing = { workspace = true }
 traits = { path = "../../traits" }
-
-[package.metadata.cargo-machete]
-ignored = ["tracing"]
+uuid = { workspace = true }
diff --git a/crates/services/integration/src/integration_trait.rs b/crates/services/integration/src/integration_trait.rs
deleted file mode 100644
index 3460915c21..0000000000
--- a/crates/services/integration/src/integration_trait.rs
+++ /dev/null
@@ -1,68 +0,0 @@
-use super::{IntegrationMediaCollection, IntegrationMediaSeen};
-use anyhow::{bail, Result};
-use async_graphql::Result as GqlResult;
-use database_models::metadata;
-use database_utils::ilike_sql;
-use enums::{MediaLot, MediaSource};
-use media_models::CommitMediaInput;
-use sea_orm::{
-    prelude::async_trait::async_trait, ColumnTrait, Condition, DatabaseConnection, EntityTrait,
-    QueryFilter,
-};
-use sea_query::{extension::postgres::PgExpr, Alias, Expr, Func};
-use std::future::Future;
-
-pub trait YankIntegration {
-    async fn yank_progress(
-        &self,
-    ) -> Result<(Vec<IntegrationMediaSeen>, Vec<IntegrationMediaCollection>)>;
-}
-
-pub trait YankIntegrationWithCommit {
-    async fn yank_progress<F>(
-        &self,
-        commit_metadata: impl Fn(CommitMediaInput) -> F,
-    ) -> Result<(Vec<IntegrationMediaSeen>, Vec<IntegrationMediaCollection>)>
-    where
-        F: Future<Output = GqlResult<metadata::Model>>;
-}
-
-pub trait PushIntegration {
-    async fn push_progress(&self) -> Result<()>;
-}
-
-#[async_trait]
-pub trait ShowIdentifier {
-    fn get_db(&self) -> &DatabaseConnection;
-
-    async fn get_show_by_episode_identifier(
-        &self,
-        series: &str,
-        episode: &str,
-    ) -> Result<metadata::Model> {
-        let db_show = metadata::Entity::find()
-            .filter(metadata::Column::Lot.eq(MediaLot::Show))
-            .filter(metadata::Column::Source.eq(MediaSource::Tmdb))
-            .filter(
-                Condition::all()
-                    .add(
-                        Expr::expr(Func::cast_as(
-                            Expr::col(metadata::Column::ShowSpecifics),
-                            Alias::new("text"),
-                        ))
-                        .ilike(ilike_sql(episode)),
-                    )
-                    .add(Expr::col(metadata::Column::Title).ilike(ilike_sql(series))),
-            )
-            .one(self.get_db())
-            .await?;
-        match db_show {
-            Some(show) => Ok(show),
-            None => bail!(
-                "No show found with Series {:#?} and Episode {:#?}",
-                series,
-                episode
-            ),
-        }
-    }
-}
diff --git a/crates/services/integration/src/integration_type.rs b/crates/services/integration/src/integration_type.rs
deleted file mode 100644
index 8709f580a9..0000000000
--- a/crates/services/integration/src/integration_type.rs
+++ /dev/null
@@ -1,13 +0,0 @@
-use enums::MediaSource;
-use providers::google_books::GoogleBooksService;
-
-pub enum IntegrationType {
-    Komga(String, String, String, MediaSource, Option<bool>),
-    Jellyfin(String),
-    Emby(String),
-    Plex(String, Option<String>),
-    Audiobookshelf(String, String, Option<bool>, GoogleBooksService),
-    Kodi(String),
-    Sonarr(String, String, i32, String, String),
-    Radarr(String, String, i32, String, String),
-}
diff --git a/crates/services/integration/src/kodi.rs b/crates/services/integration/src/kodi.rs
deleted file mode 100644
index 67a246b69f..0000000000
--- a/crates/services/integration/src/kodi.rs
+++ /dev/null
@@ -1,34 +0,0 @@
-use anyhow::bail;
-use enums::MediaSource;
-use media_models::{IntegrationMediaCollection, IntegrationMediaSeen};
-
-use super::integration_trait::YankIntegration;
-
-pub(crate) struct KodiIntegration {
-    payload: String,
-}
-impl KodiIntegration {
-    pub const fn new(payload: String) -> Self {
-        Self { payload }
-    }
-
-    async fn kodi_progress(
-        &self,
-    ) -> anyhow::Result<(Vec<IntegrationMediaSeen>, Vec<IntegrationMediaCollection>)> {
-        let mut payload = match serde_json::from_str::<IntegrationMediaSeen>(&self.payload) {
-            Ok(val) => val,
-            Err(err) => bail!(err),
-        };
-        payload.source = MediaSource::Tmdb;
-        payload.provider_watched_on = Some("Kodi".to_string());
-        Ok((vec![payload], vec![]))
-    }
-}
-
-impl YankIntegration for KodiIntegration {
-    async fn yank_progress(
-        &self,
-    ) -> anyhow::Result<(Vec<IntegrationMediaSeen>, Vec<IntegrationMediaCollection>)> {
-        self.kodi_progress().await
-    }
-}
diff --git a/crates/services/integration/src/lib.rs b/crates/services/integration/src/lib.rs
index d14877ba02..861dbb4c27 100644
--- a/crates/services/integration/src/lib.rs
+++ b/crates/services/integration/src/lib.rs
@@ -1,142 +1,276 @@
-use std::future::Future;
+use std::sync::Arc;
+
+use async_graphql::{Error, Result as GqlResult};
+use chrono::Utc;
+use common_utils::ryot_log;
+use database_models::prelude::{CollectionToEntity, Metadata};
+use database_models::{integration, prelude::Integration};
+use database_utils::user_preferences_by_id;
+use dependent_models::ImportResult;
+use dependent_utils::{commit_metadata, process_import};
+use enums::{EntityLot, IntegrationLot, IntegrationProvider, MediaLot};
+use providers::google_books::GoogleBooksService;
+use rust_decimal_macros::dec;
+use sea_orm::{ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, QueryFilter, QuerySelect};
+use sink::generic_json::GenericJsonIntegration;
+use supporting_service::SupportingService;
+use traits::TraceOk;
+use uuid::Uuid;
+
+mod push;
+mod sink;
+mod utils;
+mod yank;
 
 use crate::{
-    audiobookshelf::AudiobookshelfIntegration, emby::EmbyIntegration,
-    integration_trait::PushIntegration, integration_trait::YankIntegration,
-    integration_trait::YankIntegrationWithCommit, integration_type::IntegrationType,
-    jellyfin::JellyfinIntegration, kodi::KodiIntegration, komga::KomgaIntegration,
-    plex::PlexIntegration, radarr::RadarrIntegration, sonarr::SonarrIntegration,
+    push::{radarr::RadarrIntegration, sonarr::SonarrIntegration},
+    sink::{
+        emby::EmbyIntegration, jellyfin::JellyfinIntegration, kodi::KodiIntegration,
+        plex::PlexIntegration,
+    },
+    yank::{audiobookshelf::AudiobookshelfIntegration, komga::KomgaIntegration},
 };
-use anyhow::Result;
-use async_graphql::Result as GqlResult;
-use database_models::metadata;
-use media_models::{CommitMediaInput, IntegrationMediaCollection, IntegrationMediaSeen};
-use sea_orm::DatabaseConnection;
 
-mod audiobookshelf;
-mod emby;
-mod integration_trait;
-pub mod integration_type;
-mod jellyfin;
-mod kodi;
-mod komga;
-mod plex;
-mod radarr;
-mod sonarr;
-
-#[derive(Debug)]
-pub struct IntegrationService {
-    db: DatabaseConnection,
-}
+pub struct IntegrationService(pub Arc<SupportingService>);
 
 impl IntegrationService {
-    pub fn new(db: &DatabaseConnection) -> Self {
-        Self { db: db.clone() }
-    }
-    pub async fn push(&self, integration_type: IntegrationType) -> Result<()> {
-        match integration_type {
-            IntegrationType::Sonarr(
-                sonarr_base_url,
-                sonarr_api_key,
-                sonarr_profile_id,
-                sonarr_root_folder_path,
-                tvdb_id,
-            ) => {
-                let sonarr = SonarrIntegration::new(
-                    sonarr_base_url,
-                    sonarr_api_key,
-                    sonarr_profile_id,
-                    sonarr_root_folder_path,
-                    tvdb_id,
-                );
-                sonarr.push_progress().await
-            }
-            IntegrationType::Radarr(
-                radarr_base_url,
-                radarr_api_key,
-                radarr_profile_id,
-                radarr_root_folder_path,
-                tmdb_id,
-            ) => {
-                let radarr = RadarrIntegration::new(
-                    radarr_base_url,
-                    radarr_api_key,
-                    radarr_profile_id,
-                    radarr_root_folder_path,
-                    tmdb_id,
-                );
-                radarr.push_progress().await
-            }
-            _ => Err(anyhow::anyhow!("Unsupported integration type")),
+    async fn integration_progress_update(
+        &self,
+        integration: integration::Model,
+        updates: ImportResult,
+    ) -> GqlResult<()> {
+        let mut updates = updates;
+        updates.metadata.iter_mut().for_each(|media| {
+            media.seen_history.retain(|update| match update.progress {
+                Some(progress) if progress < integration.minimum_progress.unwrap() => {
+                    ryot_log!(
+                        debug,
+                        "Progress update for integration {} is below minimum threshold",
+                        integration.id
+                    );
+                    false
+                }
+                Some(_) => true,
+                None => false,
+            });
+            media.seen_history.iter_mut().for_each(|update| {
+                if let Some(progress) = update.progress {
+                    if progress > integration.maximum_progress.unwrap() {
+                        ryot_log!(
+                            debug,
+                            "Changing progress to 100 for integration {}",
+                            integration.id
+                        );
+                        update.progress = Some(dec!(100));
+                    }
+                }
+            });
+        });
+        if let Err(err) = process_import(&integration.user_id, updates, &self.0).await {
+            ryot_log!(debug, "Error updating progress: {:?}", err);
+        } else {
+            let mut to_update: integration::ActiveModel = integration.into();
+            to_update.last_triggered_on = ActiveValue::Set(Some(Utc::now()));
+            to_update.update(&self.0.db).await?;
         }
+        Ok(())
     }
 
-    pub async fn process_progress(
+    pub async fn process_integration_webhook(
         &self,
-        integration_type: IntegrationType,
-    ) -> Result<(Vec<IntegrationMediaSeen>, Vec<IntegrationMediaCollection>)> {
-        match integration_type {
-            IntegrationType::Jellyfin(payload) => {
-                let jellyfin = JellyfinIntegration::new(payload);
-                jellyfin.yank_progress().await
+        integration_slug: String,
+        payload: String,
+    ) -> GqlResult<String> {
+        ryot_log!(
+            debug,
+            "Processing integration webhook for slug: {}",
+            integration_slug
+        );
+        let integration = Integration::find_by_id(integration_slug)
+            .one(&self.0.db)
+            .await?
+            .ok_or_else(|| Error::new("Integration does not exist".to_owned()))?;
+        let preferences = user_preferences_by_id(&integration.user_id, &self.0).await?;
+        if integration.is_disabled.unwrap_or_default() || preferences.general.disable_integrations {
+            return Err(Error::new("Integration is disabled".to_owned()));
+        }
+        let maybe_progress_update = match integration.provider {
+            IntegrationProvider::Kodi => {
+                let kodi = KodiIntegration::new(payload);
+                kodi.yank_progress().await
             }
-            IntegrationType::Emby(payload) => {
-                let emby = EmbyIntegration::new(payload, self.db.clone());
+            IntegrationProvider::Emby => {
+                let emby = EmbyIntegration::new(payload, self.0.db.clone());
                 emby.yank_progress().await
             }
-            IntegrationType::Plex(payload, plex_user) => {
-                let plex = PlexIntegration::new(payload, plex_user, self.db.clone());
+            IntegrationProvider::Jellyfin => {
+                let jellyfin = JellyfinIntegration::new(payload);
+                jellyfin.yank_progress().await
+            }
+            IntegrationProvider::Plex => {
+                let specifics = integration.clone().provider_specifics.unwrap();
+                let plex =
+                    PlexIntegration::new(payload, specifics.plex_username, self.0.db.clone());
                 plex.yank_progress().await
             }
-            IntegrationType::Kodi(payload) => {
-                let kodi = KodiIntegration::new(payload);
-                kodi.yank_progress().await
+            IntegrationProvider::GenericJson => {
+                let generic_json = GenericJsonIntegration::new(payload);
+                generic_json.yank_progress().await
+            }
+            _ => return Err(Error::new("Unsupported integration source".to_owned())),
+        };
+        match maybe_progress_update {
+            Ok(pu) => {
+                self.integration_progress_update(integration, pu)
+                    .await
+                    .trace_ok();
+                Ok("Progress updated successfully".to_owned())
             }
-            _ => Err(anyhow::anyhow!("Unsupported integration type")),
+            Err(e) => Err(Error::new(e.to_string())),
         }
     }
 
-    pub async fn process_progress_commit<F>(
+    pub async fn handle_entity_added_to_collection_event(
         &self,
-        integration_type: IntegrationType,
-        commit_metadata: impl Fn(CommitMediaInput) -> F,
-    ) -> Result<(Vec<IntegrationMediaSeen>, Vec<IntegrationMediaCollection>)>
-    where
-        F: Future<Output = GqlResult<metadata::Model>>,
-    {
-        match integration_type {
-            IntegrationType::Audiobookshelf(
-                base_url,
-                access_token,
-                sync_to_owned_collection,
-                isbn_service,
-            ) => {
-                let audiobookshelf = AudiobookshelfIntegration::new(
-                    base_url,
-                    access_token,
-                    sync_to_owned_collection,
-                    isbn_service,
-                );
-                audiobookshelf.yank_progress(commit_metadata).await
+        user_id: String,
+        collection_to_entity_id: Uuid,
+    ) -> GqlResult<()> {
+        let cte = CollectionToEntity::find_by_id(collection_to_entity_id)
+            .one(&self.0.db)
+            .await?
+            .ok_or_else(|| Error::new("Collection to entity does not exist"))?;
+        if !matches!(cte.entity_lot, EntityLot::Metadata) {
+            return Ok(());
+        }
+        let integrations = Integration::find()
+            .filter(integration::Column::UserId.eq(user_id))
+            .filter(integration::Column::Lot.eq(IntegrationLot::Push))
+            .all(&self.0.db)
+            .await?;
+        for integration in integrations {
+            let possible_collection_ids = match integration.provider_specifics.clone() {
+                Some(s) => match integration.provider {
+                    IntegrationProvider::Radarr => s.radarr_sync_collection_ids.unwrap_or_default(),
+                    IntegrationProvider::Sonarr => s.sonarr_sync_collection_ids.unwrap_or_default(),
+                    _ => vec![],
+                },
+                None => vec![],
+            };
+            if !possible_collection_ids.contains(&cte.collection_id) {
+                continue;
             }
-            IntegrationType::Komga(
-                base_url,
-                username,
-                password,
-                provider,
-                sync_to_owned_collection,
-            ) => {
-                let komga = KomgaIntegration::new(
-                    base_url,
-                    username,
-                    password,
-                    provider,
-                    self.db.clone(),
-                    sync_to_owned_collection,
-                );
-                komga.yank_progress().await
+            let specifics = integration.provider_specifics.unwrap();
+            let metadata = Metadata::find_by_id(&cte.entity_id)
+                .one(&self.0.db)
+                .await?
+                .ok_or_else(|| Error::new("Metadata does not exist"))?;
+            let maybe_entity_id = match metadata.lot {
+                MediaLot::Show => metadata
+                    .external_identifiers
+                    .and_then(|ei| ei.tvdb_id.map(|i| i.to_string())),
+                _ => Some(metadata.identifier.clone()),
+            };
+            if let Some(entity_id) = maybe_entity_id {
+                let _push_result = match integration.provider {
+                    IntegrationProvider::Radarr => {
+                        let sonarr = SonarrIntegration::new(
+                            specifics.radarr_base_url.unwrap(),
+                            specifics.radarr_api_key.unwrap(),
+                            specifics.radarr_profile_id.unwrap(),
+                            specifics.radarr_root_folder_path.unwrap(),
+                            entity_id,
+                        );
+                        sonarr.push_progress().await
+                    }
+                    IntegrationProvider::Sonarr => {
+                        let radarr = RadarrIntegration::new(
+                            specifics.sonarr_base_url.unwrap(),
+                            specifics.sonarr_api_key.unwrap(),
+                            specifics.sonarr_profile_id.unwrap(),
+                            specifics.sonarr_root_folder_path.unwrap(),
+                            entity_id,
+                        );
+                        radarr.push_progress().await
+                    }
+                    _ => unreachable!(),
+                };
             }
-            _ => Err(anyhow::anyhow!("Unsupported integration type")),
         }
+        Ok(())
+    }
+
+    pub async fn yank_integrations_data_for_user(&self, user_id: &String) -> GqlResult<bool> {
+        let preferences = user_preferences_by_id(user_id, &self.0).await?;
+        if preferences.general.disable_integrations {
+            return Ok(false);
+        }
+        let integrations = Integration::find()
+            .filter(integration::Column::UserId.eq(user_id))
+            .filter(integration::Column::Lot.eq(IntegrationLot::Yank))
+            .all(&self.0.db)
+            .await?;
+        let mut progress_updates = vec![];
+        let mut to_update_integrations = vec![];
+        for integration in integrations.into_iter() {
+            if integration.is_disabled.unwrap_or_default() {
+                ryot_log!(debug, "Integration {} is disabled", integration.id);
+                continue;
+            }
+            let specifics = integration.clone().provider_specifics.unwrap();
+            let response = match integration.provider {
+                IntegrationProvider::Audiobookshelf => {
+                    let audiobookshelf = AudiobookshelfIntegration::new(
+                        specifics.audiobookshelf_base_url.unwrap(),
+                        specifics.audiobookshelf_token.unwrap(),
+                        integration.sync_to_owned_collection,
+                        GoogleBooksService::new(
+                            &self.0.config.books.google_books,
+                            self.0.config.frontend.page_size,
+                        )
+                        .await,
+                    );
+                    audiobookshelf
+                        .yank_progress(|input| commit_metadata(input, &self.0))
+                        .await
+                }
+                IntegrationProvider::Komga => {
+                    let komga = KomgaIntegration::new(
+                        specifics.komga_base_url.unwrap(),
+                        specifics.komga_username.unwrap(),
+                        specifics.komga_password.unwrap(),
+                        specifics.komga_provider.unwrap(),
+                        self.0.db.clone(),
+                        integration.sync_to_owned_collection,
+                    );
+                    komga.yank_progress().await
+                }
+                _ => continue,
+            };
+            if let Ok(update) = response {
+                to_update_integrations.push(integration.id.clone());
+                progress_updates.push((integration, update));
+            }
+        }
+        for (integration, progress_updates) in progress_updates.into_iter() {
+            self.integration_progress_update(integration, progress_updates)
+                .await
+                .trace_ok();
+        }
+        Ok(true)
+    }
+
+    pub async fn yank_integrations_data(&self) -> GqlResult<()> {
+        let users_with_integrations = Integration::find()
+            .filter(integration::Column::Lot.eq(IntegrationLot::Yank))
+            .select_only()
+            .column(integration::Column::UserId)
+            .into_tuple::<String>()
+            .all(&self.0.db)
+            .await?;
+        for user_id in users_with_integrations {
+            ryot_log!(debug, "Yanking integrations data for user {}", user_id);
+            self.yank_integrations_data_for_user(&user_id).await?;
+        }
+        Ok(())
     }
 }
diff --git a/crates/services/integration/src/push/mod.rs b/crates/services/integration/src/push/mod.rs
new file mode 100644
index 0000000000..a4ec084770
--- /dev/null
+++ b/crates/services/integration/src/push/mod.rs
@@ -0,0 +1,2 @@
+pub mod radarr;
+pub mod sonarr;
diff --git a/crates/services/integration/src/radarr.rs b/crates/services/integration/src/push/radarr.rs
similarity index 92%
rename from crates/services/integration/src/radarr.rs
rename to crates/services/integration/src/push/radarr.rs
index 1bbd5bfe6a..a1ec0e3b15 100644
--- a/crates/services/integration/src/radarr.rs
+++ b/crates/services/integration/src/push/radarr.rs
@@ -8,8 +8,6 @@ use radarr_api_rs::{
 };
 use traits::TraceOk;
 
-use super::integration_trait::PushIntegration;
-
 pub(crate) struct RadarrIntegration {
     radarr_base_url: String,
     radarr_api_key: String,
@@ -56,10 +54,8 @@ impl RadarrIntegration {
             .trace_ok();
         Ok(())
     }
-}
 
-impl PushIntegration for RadarrIntegration {
-    async fn push_progress(&self) -> anyhow::Result<()> {
+    pub async fn push_progress(&self) -> anyhow::Result<()> {
         self.radarr_push().await
     }
 }
diff --git a/crates/services/integration/src/sonarr.rs b/crates/services/integration/src/push/sonarr.rs
similarity index 93%
rename from crates/services/integration/src/sonarr.rs
rename to crates/services/integration/src/push/sonarr.rs
index 8c1d74da5a..e667d6d565 100644
--- a/crates/services/integration/src/sonarr.rs
+++ b/crates/services/integration/src/push/sonarr.rs
@@ -8,8 +8,6 @@ use sonarr_api_rs::{
 };
 use traits::TraceOk;
 
-use super::integration_trait::PushIntegration;
-
 pub(crate) struct SonarrIntegration {
     sonarr_base_url: String,
     sonarr_api_key: String,
@@ -58,10 +56,8 @@ impl SonarrIntegration {
             .trace_ok();
         Ok(())
     }
-}
 
-impl PushIntegration for SonarrIntegration {
-    async fn push_progress(&self) -> anyhow::Result<()> {
+    pub async fn push_progress(&self) -> anyhow::Result<()> {
         self.sonarr_push().await
     }
 }
diff --git a/crates/services/integration/src/emby.rs b/crates/services/integration/src/sink/emby.rs
similarity index 73%
rename from crates/services/integration/src/emby.rs
rename to crates/services/integration/src/sink/emby.rs
index 368c0600c5..7eaad2271c 100644
--- a/crates/services/integration/src/emby.rs
+++ b/crates/services/integration/src/sink/emby.rs
@@ -1,12 +1,13 @@
+use anyhow::{bail, Result};
+use dependent_models::ImportResult;
 use enums::{MediaLot, MediaSource};
-use media_models::{IntegrationMediaCollection, IntegrationMediaSeen};
+use media_models::{ImportOrExportMediaItem, ImportOrExportMediaItemSeen};
 use rust_decimal::Decimal;
 use rust_decimal_macros::dec;
-use sea_orm::prelude::async_trait::async_trait;
 use sea_orm::DatabaseConnection;
 use serde::{Deserialize, Serialize};
 
-use super::{integration_trait::ShowIdentifier, integration_trait::YankIntegration};
+use crate::utils::get_show_by_episode_identifier;
 
 mod models {
     use super::*;
@@ -55,9 +56,8 @@ impl EmbyIntegration {
     pub const fn new(payload: String, db: DatabaseConnection) -> Self {
         Self { payload, db }
     }
-    async fn emby_progress(
-        &self,
-    ) -> anyhow::Result<(Vec<IntegrationMediaSeen>, Vec<IntegrationMediaCollection>)> {
+
+    async fn emby_progress(&self) -> Result<ImportResult> {
         let payload: models::EmbyWebhookPayload = serde_json::from_str(&self.payload)?;
 
         let runtime = payload
@@ -85,41 +85,32 @@ impl EmbyIntegration {
                     let episode_name = payload.item.episode_name.as_ref().ok_or_else(|| {
                         anyhow::anyhow!("No episode name associated with this media")
                     })?;
-                    let db_show = self
-                        .get_show_by_episode_identifier(series_name, episode_name)
-                        .await?;
+                    let db_show =
+                        get_show_by_episode_identifier(&self.db, series_name, episode_name).await?;
                     (db_show.identifier, MediaLot::Show)
                 }
-                _ => anyhow::bail!("Only movies and shows supported"),
+                _ => bail!("Only movies and shows supported"),
             };
 
-        Ok((
-            vec![IntegrationMediaSeen {
-                identifier,
+        Ok(ImportResult {
+            metadata: vec![ImportOrExportMediaItem {
                 lot,
+                identifier,
                 source: MediaSource::Tmdb,
-                progress: position / runtime * dec!(100),
-                show_season_number: payload.item.season_number,
-                show_episode_number: payload.item.episode_number,
-                provider_watched_on: Some("Emby".to_string()),
+                seen_history: vec![ImportOrExportMediaItemSeen {
+                    progress: Some(position / runtime * dec!(100)),
+                    show_season_number: payload.item.season_number,
+                    show_episode_number: payload.item.episode_number,
+                    provider_watched_on: Some("Emby".to_string()),
+                    ..Default::default()
+                }],
                 ..Default::default()
             }],
-            vec![],
-        ))
+            ..Default::default()
+        })
     }
-}
-
-#[async_trait]
-impl ShowIdentifier for EmbyIntegration {
-    fn get_db(&self) -> &DatabaseConnection {
-        &self.db
-    }
-}
 
-impl YankIntegration for EmbyIntegration {
-    async fn yank_progress(
-        &self,
-    ) -> anyhow::Result<(Vec<IntegrationMediaSeen>, Vec<IntegrationMediaCollection>)> {
+    pub async fn yank_progress(&self) -> Result<ImportResult> {
         self.emby_progress().await
     }
 }
diff --git a/crates/services/integration/src/sink/generic_json.rs b/crates/services/integration/src/sink/generic_json.rs
new file mode 100644
index 0000000000..5069396d4c
--- /dev/null
+++ b/crates/services/integration/src/sink/generic_json.rs
@@ -0,0 +1,30 @@
+use anyhow::{bail, Result};
+use dependent_models::{CompleteExport, ImportResult};
+
+pub(crate) struct GenericJsonIntegration {
+    payload: String,
+}
+impl GenericJsonIntegration {
+    pub const fn new(payload: String) -> Self {
+        Self { payload }
+    }
+
+    async fn generic_json_progress(&self) -> Result<ImportResult> {
+        let payload = match serde_json::from_str::<CompleteExport>(&self.payload) {
+            Ok(val) => val,
+            Err(err) => bail!(err),
+        };
+        Ok(ImportResult {
+            metadata: payload.media.unwrap_or_default(),
+            people: payload.people.unwrap_or_default(),
+            measurements: payload.measurements.unwrap_or_default(),
+            metadata_groups: payload.media_groups.unwrap_or_default(),
+            application_workouts: payload.workouts.unwrap_or_default(),
+            ..Default::default()
+        })
+    }
+
+    pub async fn yank_progress(&self) -> Result<ImportResult> {
+        self.generic_json_progress().await
+    }
+}
diff --git a/crates/services/integration/src/jellyfin.rs b/crates/services/integration/src/sink/jellyfin.rs
similarity index 79%
rename from crates/services/integration/src/jellyfin.rs
rename to crates/services/integration/src/sink/jellyfin.rs
index 8ceea0af60..feced79b92 100644
--- a/crates/services/integration/src/jellyfin.rs
+++ b/crates/services/integration/src/sink/jellyfin.rs
@@ -1,12 +1,11 @@
 use anyhow::{bail, Result};
+use dependent_models::ImportResult;
 use enums::{MediaLot, MediaSource};
-use media_models::{IntegrationMediaCollection, IntegrationMediaSeen};
+use media_models::{ImportOrExportMediaItem, ImportOrExportMediaItemSeen};
 use rust_decimal::Decimal;
 use rust_decimal_macros::dec;
 use serde::{Deserialize, Serialize};
 
-use super::integration_trait::YankIntegration;
-
 mod models {
     use super::*;
 
@@ -56,9 +55,7 @@ impl JellyfinIntegration {
         Self { payload }
     }
 
-    async fn jellyfin_progress(
-        &self,
-    ) -> Result<(Vec<IntegrationMediaSeen>, Vec<IntegrationMediaCollection>)> {
+    async fn jellyfin_progress(&self) -> Result<ImportResult> {
         let payload = serde_json::from_str::<models::JellyfinWebhookPayload>(&self.payload)?;
         let identifier = payload
             .item
@@ -91,26 +88,25 @@ impl JellyfinIntegration {
             _ => bail!("Only movies and shows supported"),
         };
 
-        Ok((
-            vec![IntegrationMediaSeen {
-                identifier,
+        Ok(ImportResult {
+            metadata: vec![ImportOrExportMediaItem {
                 lot,
+                identifier,
                 source: MediaSource::Tmdb,
-                progress: position / runtime * dec!(100),
-                show_season_number: payload.item.season_number,
-                show_episode_number: payload.item.episode_number,
-                provider_watched_on: Some("Jellyfin".to_string()),
+                seen_history: vec![ImportOrExportMediaItemSeen {
+                    progress: Some(position / runtime * dec!(100)),
+                    show_season_number: payload.item.season_number,
+                    show_episode_number: payload.item.episode_number,
+                    provider_watched_on: Some("Jellyfin".to_string()),
+                    ..Default::default()
+                }],
                 ..Default::default()
             }],
-            vec![],
-        ))
+            ..Default::default()
+        })
     }
-}
 
-impl YankIntegration for JellyfinIntegration {
-    async fn yank_progress(
-        &self,
-    ) -> Result<(Vec<IntegrationMediaSeen>, Vec<IntegrationMediaCollection>)> {
+    pub async fn yank_progress(&self) -> Result<ImportResult> {
         self.jellyfin_progress().await
     }
 }
diff --git a/crates/services/integration/src/sink/kodi.rs b/crates/services/integration/src/sink/kodi.rs
new file mode 100644
index 0000000000..2b8864dfc0
--- /dev/null
+++ b/crates/services/integration/src/sink/kodi.rs
@@ -0,0 +1,52 @@
+use anyhow::{bail, Result};
+use dependent_models::ImportResult;
+use enums::{MediaLot, MediaSource};
+use media_models::{ImportOrExportMediaItem, ImportOrExportMediaItemSeen};
+use rust_decimal::Decimal;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+struct IntegrationMediaSeen {
+    identifier: String,
+    lot: MediaLot,
+    progress: Decimal,
+    show_season_number: Option<i32>,
+    show_episode_number: Option<i32>,
+}
+
+pub(crate) struct KodiIntegration {
+    payload: String,
+}
+impl KodiIntegration {
+    pub const fn new(payload: String) -> Self {
+        Self { payload }
+    }
+
+    async fn kodi_progress(&self) -> Result<ImportResult> {
+        let payload = match serde_json::from_str::<IntegrationMediaSeen>(&self.payload) {
+            Ok(val) => val,
+            Err(err) => bail!(err),
+        };
+
+        Ok(ImportResult {
+            metadata: vec![ImportOrExportMediaItem {
+                lot: payload.lot,
+                source: MediaSource::Tmdb,
+                identifier: payload.identifier,
+                seen_history: vec![ImportOrExportMediaItemSeen {
+                    progress: Some(payload.progress),
+                    show_season_number: payload.show_season_number,
+                    show_episode_number: payload.show_episode_number,
+                    provider_watched_on: Some("Kodi".to_string()),
+                    ..Default::default()
+                }],
+                ..Default::default()
+            }],
+            ..Default::default()
+        })
+    }
+
+    pub async fn yank_progress(&self) -> Result<ImportResult> {
+        self.kodi_progress().await
+    }
+}
diff --git a/crates/services/integration/src/sink/mod.rs b/crates/services/integration/src/sink/mod.rs
new file mode 100644
index 0000000000..4b113e6634
--- /dev/null
+++ b/crates/services/integration/src/sink/mod.rs
@@ -0,0 +1,5 @@
+pub mod emby;
+pub mod generic_json;
+pub mod jellyfin;
+pub mod kodi;
+pub mod plex;
diff --git a/crates/services/integration/src/plex.rs b/crates/services/integration/src/sink/plex.rs
similarity index 76%
rename from crates/services/integration/src/plex.rs
rename to crates/services/integration/src/sink/plex.rs
index 8c59673254..c72c96e2f5 100644
--- a/crates/services/integration/src/plex.rs
+++ b/crates/services/integration/src/sink/plex.rs
@@ -1,15 +1,15 @@
-use anyhow::{bail, Context};
-use async_graphql::async_trait::async_trait;
+use anyhow::{bail, Context, Result};
 use common_utils::ryot_log;
+use dependent_models::ImportResult;
 use enums::{MediaLot, MediaSource};
-use media_models::{IntegrationMediaCollection, IntegrationMediaSeen};
+use media_models::{ImportOrExportMediaItem, ImportOrExportMediaItemSeen};
 use regex::Regex;
 use rust_decimal::Decimal;
 use rust_decimal_macros::dec;
 use sea_orm::DatabaseConnection;
 use serde::{Deserialize, Serialize};
 
-use super::{integration_trait::ShowIdentifier, integration_trait::YankIntegration};
+use crate::utils::get_show_by_episode_identifier;
 
 mod models {
     use super::*;
@@ -67,7 +67,7 @@ impl PlexIntegration {
         }
     }
 
-    fn parse_payload(payload: &str) -> anyhow::Result<models::PlexWebhookPayload> {
+    fn parse_payload(payload: &str) -> Result<models::PlexWebhookPayload> {
         let payload_regex = Regex::new(r"\{.*\}").unwrap();
         let json_payload = payload_regex
             .find(payload)
@@ -76,7 +76,7 @@ impl PlexIntegration {
         serde_json::from_str(json_payload).context("Error during JSON payload deserialization")
     }
 
-    fn get_tmdb_identifier(guids: &[models::PlexWebhookMetadataGuid]) -> anyhow::Result<&str> {
+    fn get_tmdb_identifier(guids: &[models::PlexWebhookMetadataGuid]) -> Result<&str> {
         guids
             .iter()
             .find(|g| g.id.starts_with("tmdb://"))
@@ -88,21 +88,20 @@ impl PlexIntegration {
         &self,
         metadata: &'a models::PlexWebhookMetadataPayload,
         identifier: &'a str,
-    ) -> anyhow::Result<(String, MediaLot)> {
+    ) -> Result<(String, MediaLot)> {
         match metadata.item_type.as_str() {
             "movie" => Ok((identifier.to_owned(), MediaLot::Movie)),
             "episode" => {
                 let series_name = metadata.show_name.as_ref().context("Show name missing")?;
-                let db_show = self
-                    .get_show_by_episode_identifier(series_name, identifier)
-                    .await?;
+                let db_show =
+                    get_show_by_episode_identifier(&self.db, series_name, identifier).await?;
                 Ok((db_show.identifier, MediaLot::Show))
             }
             _ => bail!("Only movies and shows supported"),
         }
     }
 
-    fn calculate_progress(payload: &models::PlexWebhookPayload) -> anyhow::Result<Decimal> {
+    fn calculate_progress(payload: &models::PlexWebhookPayload) -> Result<Decimal> {
         match payload.metadata.view_offset {
             Some(offset) => Ok(offset / payload.metadata.duration * dec!(100)),
             None if payload.event_type == "media.scrobble" => Ok(dec!(100)),
@@ -110,9 +109,7 @@ impl PlexIntegration {
         }
     }
 
-    async fn plex_progress(
-        &self,
-    ) -> anyhow::Result<(Vec<IntegrationMediaSeen>, Vec<IntegrationMediaCollection>)> {
+    async fn plex_progress(&self) -> Result<ImportResult> {
         ryot_log!(debug, "Processing Plex payload {:#?}", self.payload);
 
         let payload = Self::parse_payload(&self.payload)?;
@@ -135,33 +132,25 @@ impl PlexIntegration {
         let (identifier, lot) = self.get_media_info(&payload.metadata, identifier).await?;
         let progress = Self::calculate_progress(&payload)?;
 
-        Ok((
-            vec![IntegrationMediaSeen {
-                identifier,
+        Ok(ImportResult {
+            metadata: vec![ImportOrExportMediaItem {
                 lot,
+                identifier,
                 source: MediaSource::Tmdb,
-                progress,
-                provider_watched_on: Some("Plex".to_string()),
-                show_season_number: payload.metadata.season_number,
-                show_episode_number: payload.metadata.episode_number,
+                seen_history: vec![ImportOrExportMediaItemSeen {
+                    progress: Some(progress),
+                    provider_watched_on: Some("Plex".to_string()),
+                    show_season_number: payload.metadata.season_number,
+                    show_episode_number: payload.metadata.episode_number,
+                    ..Default::default()
+                }],
                 ..Default::default()
             }],
-            vec![],
-        ))
-    }
-}
-
-#[async_trait]
-impl ShowIdentifier for PlexIntegration {
-    fn get_db(&self) -> &DatabaseConnection {
-        &self.db
+            ..Default::default()
+        })
     }
-}
 
-impl YankIntegration for PlexIntegration {
-    async fn yank_progress(
-        &self,
-    ) -> anyhow::Result<(Vec<IntegrationMediaSeen>, Vec<IntegrationMediaCollection>)> {
+    pub async fn yank_progress(&self) -> Result<ImportResult> {
         self.plex_progress().await
     }
 }
diff --git a/crates/services/integration/src/utils.rs b/crates/services/integration/src/utils.rs
new file mode 100644
index 0000000000..d11bb786cb
--- /dev/null
+++ b/crates/services/integration/src/utils.rs
@@ -0,0 +1,37 @@
+use anyhow::{bail, Result};
+use database_models::metadata;
+use database_utils::ilike_sql;
+use enums::{MediaLot, MediaSource};
+use sea_orm::{ColumnTrait, Condition, DatabaseConnection, EntityTrait, QueryFilter};
+use sea_query::{extension::postgres::PgExpr, Alias, Expr, Func};
+
+pub async fn get_show_by_episode_identifier(
+    db: &DatabaseConnection,
+    series: &str,
+    episode: &str,
+) -> Result<metadata::Model> {
+    let db_show = metadata::Entity::find()
+        .filter(metadata::Column::Lot.eq(MediaLot::Show))
+        .filter(metadata::Column::Source.eq(MediaSource::Tmdb))
+        .filter(
+            Condition::all()
+                .add(
+                    Expr::expr(Func::cast_as(
+                        Expr::col(metadata::Column::ShowSpecifics),
+                        Alias::new("text"),
+                    ))
+                    .ilike(ilike_sql(episode)),
+                )
+                .add(Expr::col(metadata::Column::Title).ilike(ilike_sql(series))),
+        )
+        .one(db)
+        .await?;
+    match db_show {
+        Some(show) => Ok(show),
+        None => bail!(
+            "No show found with Series {:#?} and Episode {:#?}",
+            series,
+            episode
+        ),
+    }
+}
diff --git a/crates/services/integration/src/audiobookshelf.rs b/crates/services/integration/src/yank/audiobookshelf.rs
similarity index 90%
rename from crates/services/integration/src/audiobookshelf.rs
rename to crates/services/integration/src/yank/audiobookshelf.rs
index fae2fdd1b2..9092fe3434 100644
--- a/crates/services/integration/src/audiobookshelf.rs
+++ b/crates/services/integration/src/yank/audiobookshelf.rs
@@ -1,20 +1,19 @@
 use std::future::Future;
 
-use anyhow::anyhow;
+use anyhow::{anyhow, Result};
 use application_utils::get_base_http_client;
 use async_graphql::Result as GqlResult;
 use common_models::DefaultCollection;
 use common_utils::ryot_log;
 use database_models::metadata;
+use dependent_models::ImportResult;
 use enums::{MediaLot, MediaSource};
-use media_models::{CommitMediaInput, IntegrationMediaCollection, IntegrationMediaSeen};
+use media_models::{CommitMediaInput, ImportOrExportMediaItem, ImportOrExportMediaItemSeen};
 use providers::google_books::GoogleBooksService;
 use reqwest::header::{HeaderValue, AUTHORIZATION};
 use rust_decimal_macros::dec;
 use specific_models::audiobookshelf::{self, LibrariesListResponse, ListResponse};
 
-use super::integration_trait::YankIntegrationWithCommit;
-
 pub(crate) struct AudiobookshelfIntegration {
     base_url: String,
     access_token: String,
@@ -36,13 +35,11 @@ impl AudiobookshelfIntegration {
             isbn_service,
         }
     }
-}
 
-impl YankIntegrationWithCommit for AudiobookshelfIntegration {
-    async fn yank_progress<F>(
+    pub async fn yank_progress<F>(
         &self,
         commit_metadata: impl Fn(CommitMediaInput) -> F,
-    ) -> anyhow::Result<(Vec<IntegrationMediaSeen>, Vec<IntegrationMediaCollection>)>
+    ) -> Result<ImportResult>
     where
         F: Future<Output = GqlResult<metadata::Model>>,
     {
@@ -63,7 +60,7 @@ impl YankIntegrationWithCommit for AudiobookshelfIntegration {
 
         ryot_log!(debug, "Got response for items in progress {:?}", resp);
 
-        let mut media_items = vec![];
+        let mut result = ImportResult::default();
 
         for item in resp.library_items.iter() {
             let metadata = item.media.clone().unwrap().metadata;
@@ -164,13 +161,16 @@ impl YankIntegrationWithCommit for AudiobookshelfIntegration {
                     } else {
                         resp.progress
                     };
-                    media_items.push(IntegrationMediaSeen {
+                    result.metadata.push(ImportOrExportMediaItem {
                         lot,
                         source,
                         identifier,
-                        podcast_episode_number,
-                        progress: progress * dec!(100),
-                        provider_watched_on: Some("Audiobookshelf".to_string()),
+                        seen_history: vec![ImportOrExportMediaItemSeen {
+                            podcast_episode_number,
+                            progress: Some(progress * dec!(100)),
+                            provider_watched_on: Some("Audiobookshelf".to_string()),
+                            ..Default::default()
+                        }],
                         ..Default::default()
                     });
                 }
@@ -180,7 +180,6 @@ impl YankIntegrationWithCommit for AudiobookshelfIntegration {
                 }
             };
         }
-        let mut collection_updates = vec![];
         if let Some(true) = self.sync_to_owned_collection {
             let libraries_resp = client
                 .get("libraries")
@@ -217,15 +216,16 @@ impl YankIntegrationWithCommit for AudiobookshelfIntegration {
                         } else {
                             continue;
                         };
-                    collection_updates.push(IntegrationMediaCollection {
+                    result.metadata.push(ImportOrExportMediaItem {
                         identifier,
                         lot,
                         source,
-                        collection: DefaultCollection::Owned.to_string(),
+                        collections: vec![DefaultCollection::Owned.to_string()],
+                        ..Default::default()
                     });
                 }
             }
         }
-        Ok((media_items, collection_updates))
+        Ok(result)
     }
 }
diff --git a/crates/services/integration/src/komga.rs b/crates/services/integration/src/yank/komga.rs
similarity index 77%
rename from crates/services/integration/src/komga.rs
rename to crates/services/integration/src/yank/komga.rs
index f830876828..fbcb1bf327 100644
--- a/crates/services/integration/src/komga.rs
+++ b/crates/services/integration/src/yank/komga.rs
@@ -3,26 +3,25 @@ use std::{
     sync::{Mutex, OnceLock},
 };
 
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, bail, Context, Result};
 use application_utils::get_base_http_client;
 use async_graphql::futures_util::{stream, StreamExt};
 use common_models::DefaultCollection;
 use common_utils::ryot_log;
 use database_models::{metadata, prelude::Metadata};
+use dependent_models::ImportResult;
 use enums::{MediaLot, MediaSource};
 use eventsource_stream::Eventsource;
+use itertools::Itertools;
+use media_models::{CommitMediaInput, ImportOrExportMediaItem, ImportOrExportMediaItemSeen};
 use reqwest::Url;
-use rust_decimal::{
-    prelude::{FromPrimitive, Zero},
-    Decimal,
-};
+use rust_decimal::{prelude::FromPrimitive, Decimal};
+use rust_decimal_macros::dec;
 use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
 use sea_query::Expr;
 use serde::{de::DeserializeOwned, Deserialize, Serialize};
 use tokio::sync::{mpsc, mpsc::error::TryRecvError, mpsc::UnboundedReceiver};
 
-use super::{integration_trait::YankIntegration, IntegrationMediaCollection, IntegrationMediaSeen};
-
 mod komga_book {
     use super::*;
 
@@ -82,15 +81,15 @@ mod komga_series {
     }
 
     impl Metadata {
-        /// Provided with a url this will extract the ID number from it. For example the url
-        /// https://myanimelist.net/manga/116778 will extract 116778
+        /// Provided with a url this will extract the ID number from it. For example the
+        /// url https://myanimelist.net/manga/116778 will extract 116778
         ///
-        /// Currently only works for myanimelist and anilist as mangaupdates doesn't store the ID
+        /// Currently only works for MAL and anilist as MangaUpdates doesn't store the ID
         /// in the url
         ///
         /// # Arguments
         ///
-        /// * `url`: The url to extact from
+        /// * `url`: The url to extract from
         ///
         /// returns: The ID number if the extraction is successful
         fn extract_id(&self, url: String) -> Option<String> {
@@ -106,11 +105,11 @@ mod komga_series {
 
         /// Extracts the list of providers with a MediaSource,ID Tuple
         ///
-        /// Currently only works for myanimelist and anilist as mangaupdates doesn't store
-        /// the ID in the url
+        /// Currently only works for MAL and Anilist as MangaUpdates doesn't store the ID
+        /// in the url
         ///
-        /// Requires that the metadata is stored with the label anilist or myanimelist
-        /// other spellings wont work
+        /// Requires that the metadata is stored with the label Anilist or MAL other
+        /// spellings wont work
         ///
         /// returns: list of providers with a MediaSource, ID Tuple
         pub fn find_providers(&self) -> Vec<(MediaSource, Option<String>)> {
@@ -187,26 +186,28 @@ pub(crate) struct KomgaIntegration {
     base_url: String,
     username: String,
     password: String,
-    provider: MediaSource,
+    source: MediaSource,
     db: DatabaseConnection,
     sync_to_owned_collection: Option<bool>,
 }
 
+type ProcessEventReturn = (CommitMediaInput, ImportOrExportMediaItemSeen);
+
 impl KomgaIntegration {
     pub fn new(
         base_url: String,
         username: String,
         password: String,
-        provider: MediaSource,
+        source: MediaSource,
         db: DatabaseConnection,
         sync_to_owned_collection: Option<bool>,
     ) -> Self {
         Self {
+            db,
+            source,
             base_url,
             username,
             password,
-            provider,
-            db,
             sync_to_owned_collection,
         }
     }
@@ -276,7 +277,7 @@ impl KomgaIntegration {
     ///
     /// # Arguments
     ///
-    /// * `client`: Prepopulated client please use `get_base_http_client` to construct this
+    /// * `client`: Pre-populated client. Use `get_base_http_client` to construct this
     /// * `cookie`: The komga cookie with the remember-me included
     /// * `api_endpoint`: Endpoint which comes after the base_url doesn't require a
     ///   prepended `/`
@@ -319,7 +320,7 @@ impl KomgaIntegration {
         if !providers.is_empty() {
             Ok(providers
                 .iter()
-                .find(|x| x.0 == self.provider)
+                .find(|x| x.0 == self.source)
                 .cloned()
                 .or_else(|| providers.first().cloned())
                 .unwrap_or_default())
@@ -336,56 +337,56 @@ impl KomgaIntegration {
         }
     }
 
-    fn calculate_percentage(curr_page: i32, total_page: i32) -> Decimal {
+    fn calculate_percentage(current_page: i32, total_page: i32) -> Decimal {
         if total_page == 0 {
-            return Decimal::zero();
+            return dec!(0);
         }
-
-        let percentage = (curr_page as f64 / total_page as f64) * 100.0;
-
-        Decimal::from_f64(percentage).unwrap_or(Decimal::zero())
+        let percentage = (current_page as f64 / total_page as f64) * 100.0;
+        Decimal::from_f64(percentage).unwrap_or(dec!(0))
     }
 
-    async fn process_events(&self, data: komga_events::Data) -> Option<IntegrationMediaSeen> {
+    async fn process_events(&self, data: komga_events::Data) -> Result<ProcessEventReturn> {
         let url = format!("{}/api/v1", self.base_url);
         let client = get_base_http_client(None);
 
         let book: komga_book::Item = self
             .fetch_api(&client, &format!("{}/books", &url), &data.book_id)
-            .await
-            .ok()?;
+            .await?;
         let series: komga_series::Item = self
             .fetch_api(&client, &format!("{}/series", &url), &book.series_id)
-            .await
-            .ok()?;
+            .await?;
 
-        let (source, id) = self.find_provider_and_id(&series).await.ok()?;
+        let (source, id) = self.find_provider_and_id(&series).await?;
 
         let Some(id) = id else {
-            ryot_log!(
-                debug,
+            let msg = format!(
                 "No MAL URL or database entry found for manga: {}",
                 series.name
             );
-            return None;
+            ryot_log!(debug, msg);
+            bail!(msg)
         };
 
-        Some(IntegrationMediaSeen {
-            identifier: id,
-            lot: MediaLot::Manga,
-            source,
-            manga_chapter_number: Some(book.metadata.number.parse().unwrap_or_default()),
-            progress: Self::calculate_percentage(book.read_progress.page, book.media.pages_count),
-            provider_watched_on: Some("Komga".to_string()),
-            ..Default::default()
-        })
+        Ok((
+            CommitMediaInput {
+                source,
+                identifier: id,
+                lot: MediaLot::Manga,
+                force_update: None,
+            },
+            ImportOrExportMediaItemSeen {
+                manga_chapter_number: Some(book.metadata.number.parse().unwrap_or_default()),
+                progress: Some(Self::calculate_percentage(
+                    book.read_progress.page,
+                    book.media.pages_count,
+                )),
+                provider_watched_on: Some("Komga".to_string()),
+                ..Default::default()
+            },
+        ))
     }
 
-    async fn sync_manga_collection(&self) -> Result<Vec<IntegrationMediaCollection>> {
-        if self.sync_to_owned_collection != Some(true) {
-            return Ok(Vec::new());
-        }
-
+    async fn sync_manga_collection(&self, result: &mut ImportResult) -> Result<()> {
         let url = &format!("{}/api/v1", self.base_url);
         let client = get_base_http_client(None);
 
@@ -398,39 +399,35 @@ impl KomgaIntegration {
             .json()
             .await?;
 
-        // Hashmap for If you have the same manga in multiple languages it will appear
-        // multiple times this prevents us from double commiting an identifier
-        let unique_collection_updates: HashMap<String, IntegrationMediaCollection> =
-            stream::iter(series.content)
-                .filter_map(|book| async move {
-                    match self.find_provider_and_id(&book).await {
-                        Ok((source, Some(id))) => Some((
-                            id.clone(),
-                            IntegrationMediaCollection {
-                                identifier: id,
-                                lot: MediaLot::Manga,
-                                source,
-                                collection: DefaultCollection::Owned.to_string(),
-                            },
-                        )),
-                        _ => {
-                            tracing::debug!(
-                                "No URL or database entry found for manga: {}",
-                                book.name
-                            );
-                            None
-                        }
+        // Hashmap for if you have the same manga in multiple languages it will appear
+        // multiple times this prevents us from double committing an identifier
+        let unique_collection_updates: HashMap<String, _> = stream::iter(series.content)
+            .filter_map(|book| async move {
+                match self.find_provider_and_id(&book).await {
+                    Ok((source, Some(id))) => Some((
+                        id.clone(),
+                        ImportOrExportMediaItem {
+                            identifier: id,
+                            lot: MediaLot::Manga,
+                            source,
+                            collections: vec![DefaultCollection::Owned.to_string()],
+                            ..Default::default()
+                        },
+                    )),
+                    _ => {
+                        tracing::debug!("No URL or database entry found for manga: {}", book.name);
+                        None
                     }
-                })
-                .collect()
-                .await;
-
-        Ok(unique_collection_updates.into_values().collect())
+                }
+            })
+            .collect()
+            .await;
+        result.metadata.extend(unique_collection_updates.into_values());
+        Ok(())
     }
 
-    async fn komga_progress(
-        &self,
-    ) -> Result<(Vec<IntegrationMediaSeen>, Vec<IntegrationMediaCollection>)> {
+    async fn komga_progress(&self) -> Result<ImportResult> {
+        let mut result = ImportResult::default();
         // DEV: This object needs global lifetime so we can continue to use the receiver if
         // we ever create more SSE Objects we may want to implement a higher level
         // Controller or make a housekeeping function to make sure the background threads
@@ -464,7 +461,7 @@ impl KomgaIntegration {
         }
 
         // Use hashmap here so we don't dupe pulls for a single book
-        let mut unique_media_items: HashMap<String, IntegrationMediaSeen> = HashMap::new();
+        let mut unique_media_items: HashMap<String, ProcessEventReturn> = HashMap::new();
 
         if let Some(mut recv) = receiver {
             loop {
@@ -473,7 +470,7 @@ impl KomgaIntegration {
                         ryot_log!(debug, "Received event {:?}", event);
                         match unique_media_items.entry(event.book_id.clone()) {
                             Entry::Vacant(entry) => {
-                                if let Some(processed_event) =
+                                if let Ok(processed_event) =
                                     self.process_events(event.clone()).await
                                 {
                                     entry.insert(processed_event);
@@ -489,7 +486,7 @@ impl KomgaIntegration {
                         }
                     }
                     Err(TryRecvError::Empty) => break,
-                    Err(e) => return Err(anyhow::anyhow!("Receiver error: {}", e)),
+                    Err(e) => bail!("Receiver error: {}", e),
                 }
             }
 
@@ -497,20 +494,26 @@ impl KomgaIntegration {
             mutex_receiver.lock().unwrap().replace(recv);
         }
 
-        let mut media_items: Vec<IntegrationMediaSeen> = unique_media_items.into_values().collect();
-        media_items.sort_by(|a, b| a.manga_chapter_number.cmp(&b.manga_chapter_number));
+        let media_items = unique_media_items.into_values().collect_vec();
         ryot_log!(debug, "Media Items: {:?}", media_items);
+        media_items.into_iter().for_each(|(commit, hist)| {
+            result.metadata.push(ImportOrExportMediaItem {
+                lot: commit.lot,
+                source: commit.source,
+                identifier: commit.identifier,
+                seen_history: vec![hist],
+                ..Default::default()
+            });
+        });
 
-        let collection_updates = self.sync_manga_collection().await?;
+        if let Some(true) = self.sync_to_owned_collection {
+            self.sync_manga_collection(&mut result).await?;
+        }
 
-        Ok((media_items, collection_updates))
+        Ok(result)
     }
-}
 
-impl YankIntegration for KomgaIntegration {
-    async fn yank_progress(
-        &self,
-    ) -> Result<(Vec<IntegrationMediaSeen>, Vec<IntegrationMediaCollection>)> {
+    pub async fn yank_progress(&self) -> Result<ImportResult> {
         self.komga_progress().await
     }
 }
diff --git a/crates/services/integration/src/yank/mod.rs b/crates/services/integration/src/yank/mod.rs
new file mode 100644
index 0000000000..e35753d982
--- /dev/null
+++ b/crates/services/integration/src/yank/mod.rs
@@ -0,0 +1,2 @@
+pub mod audiobookshelf;
+pub mod komga;
diff --git a/crates/services/miscellaneous/Cargo.toml b/crates/services/miscellaneous/Cargo.toml
index ac86404eb1..3507dd0b89 100644
--- a/crates/services/miscellaneous/Cargo.toml
+++ b/crates/services/miscellaneous/Cargo.toml
@@ -8,25 +8,20 @@ apalis = { workspace = true }
 application-utils = { path = "../../utils/application" }
 async-graphql = { workspace = true }
 background = { path = "../../background" }
-cache-service = { path = "../cache" }
 chrono = { workspace = true }
-chrono-tz = { workspace = true }
 common-models = { path = "../../models/common" }
 common-utils = { path = "../../utils/common" }
-config = { path = "../../config" }
 database-models = { path = "../../models/database" }
 database-utils = { path = "../../utils/database" }
 dependent-models = { path = "../../models/dependent" }
+dependent-utils = { path = "../../utils/dependent" }
 enums = { path = "../../enums" }
 env-utils = { path = "../../utils/env" }
-file-storage-service = { path = "../../services/file-storage" }
 futures = { workspace = true }
-integration-service = { path = "../../services/integration" }
 itertools = { workspace = true }
 markdown = { workspace = true }
 media-models = { path = "../../models/media" }
 migrations = { path = "../../migrations" }
-moka = { version = "=0.12.8", features = ["future"] }
 nanoid = { workspace = true }
 notification-service = { path = "../../services/notification" }
 providers = { path = "../../providers" }
@@ -37,6 +32,7 @@ sea-query = { workspace = true }
 serde = { workspace = true }
 serde_json = { workspace = true }
 slug = { workspace = true }
+supporting-service = { path = "../supporting" }
 tokio = { workspace = true }
 tracing = { workspace = true }
 traits = { path = "../../traits" }
diff --git a/crates/services/miscellaneous/src/lib.rs b/crates/services/miscellaneous/src/lib.rs
index 47cf1c7239..0e52fc148b 100644
--- a/crates/services/miscellaneous/src/lib.rs
+++ b/crates/services/miscellaneous/src/lib.rs
@@ -1,15 +1,12 @@
 use std::{
     collections::{HashMap, HashSet},
-    hash::Hash as StdHash,
-    iter::zip,
     sync::Arc,
 };
 
-use apalis::prelude::{MemoryStorage, MessageQueue};
+use apalis::prelude::MessageQueue;
 use application_utils::get_current_date;
-use async_graphql::{Enum, Error, Result};
+use async_graphql::{Error, Result};
 use background::{ApplicationJob, CoreApplicationJob};
-use cache_service::CacheService;
 use chrono::{Days, Duration as ChronoDuration, NaiveDate, Utc};
 use common_models::{
     ApplicationCacheKey, BackendError, BackgroundJob, ChangeCollectionToEntityInput,
@@ -22,20 +19,19 @@ use common_utils::{
 use database_models::{
     access_link, application_cache, calendar_event, collection, collection_to_entity,
     functions::{associate_user_with_entity, get_user_to_entity_association},
-    genre, import_report, integration, metadata, metadata_group, metadata_to_genre,
-    metadata_to_metadata, metadata_to_metadata_group, metadata_to_person, monitored_entity,
-    notification_platform, person,
+    genre, import_report, metadata, metadata_group, metadata_to_genre, metadata_to_metadata,
+    metadata_to_metadata_group, metadata_to_person, monitored_entity, notification_platform,
+    person,
     prelude::{
         AccessLink, ApplicationCache, CalendarEvent, Collection, CollectionToEntity, Genre,
-        ImportReport, Integration, Metadata, MetadataGroup, MetadataToGenre, MetadataToMetadata,
+        ImportReport, Metadata, MetadataGroup, MetadataToGenre, MetadataToMetadata,
         MetadataToMetadataGroup, MetadataToPerson, MonitoredEntity, NotificationPlatform, Person,
         QueuedNotification, Review, Seen, User, UserToEntity,
     },
     queued_notification, review, seen, user, user_to_entity,
 };
 use database_utils::{
-    add_entity_to_collection, admin_account_guard, apply_collection_filter,
-    calculate_user_activities_and_summary, deploy_job_to_re_evaluate_user_workouts,
+    add_entity_to_collection, 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,
     user_preferences_by_id,
@@ -44,14 +40,19 @@ use dependent_models::{
     CoreDetails, GenreDetails, MetadataBaseData, MetadataGroupDetails, PersonDetails,
     SearchResults, UserMetadataDetails, UserMetadataGroupDetails, UserPersonDetails,
 };
+use dependent_utils::{
+    after_media_seen_tasks, commit_metadata, commit_metadata_group_internal,
+    commit_metadata_internal, commit_person, create_partial_metadata, deploy_background_job,
+    deploy_update_metadata_job, get_entities_monitored_by, get_metadata_provider,
+    get_openlibrary_service, get_tmdb_non_media_service, is_metadata_finished_by_user, post_review,
+    progress_update, queue_media_state_changed_notification_for_user,
+    queue_notifications_to_user_platforms, update_metadata, update_metadata_and_notify_users,
+};
 use enums::{
-    EntityLot, IntegrationLot, IntegrationProvider, MediaLot, MediaSource,
-    MetadataToMetadataRelation, SeenState, UserToMediaReason, Visibility,
+    EntityLot, MediaLot, MediaSource, MetadataToMetadataRelation, SeenState, UserToMediaReason,
 };
 use env_utils::APP_VERSION;
-use file_storage_service::FileStorageService;
 use futures::TryStreamExt;
-use integration_service::{integration_type::IntegrationType, IntegrationService};
 use itertools::Itertools;
 use markdown::{to_html_with_options as markdown_to_html_opts, CompileOptions, Options};
 use media_models::{
@@ -59,41 +60,37 @@ use media_models::{
     CreateCustomMetadataInput, CreateOrUpdateReviewInput, CreateReviewCommentInput,
     GenreDetailsInput, GenreListItem, GraphqlCalendarEvent, GraphqlMediaAssets,
     GraphqlMetadataDetails, GraphqlMetadataGroup, GraphqlVideoAsset, GroupedCalendarEvent,
-    ImportOrExportItemReviewComment, IntegrationMediaSeen, MediaAssociatedPersonStateChanges,
-    MediaDetails, MediaGeneralFilter, MediaSortBy, MetadataCreator, MetadataCreatorGroupedByRole,
+    ImportOrExportItemReviewComment, MediaAssociatedPersonStateChanges, MediaDetails,
+    MediaGeneralFilter, MediaSortBy, MetadataCreator, MetadataCreatorGroupedByRole,
     MetadataFreeCreator, MetadataGroupSearchInput, MetadataGroupSearchItem,
     MetadataGroupsListInput, MetadataImage, MetadataImageForMediaDetails, MetadataListInput,
     MetadataPartialDetails, MetadataSearchInput, MetadataSearchItemResponse, MetadataVideo,
-    MetadataVideoSource, PartialMetadata, PartialMetadataPerson, PartialMetadataWithoutId,
-    PeopleListInput, PeopleSearchInput, PeopleSearchItem, PersonAndMetadataGroupsSortBy,
-    PersonDetailsGroupedByRole, PersonDetailsItemWithCharacter, PodcastSpecifics,
-    ProgressUpdateError, ProgressUpdateErrorVariant, ProgressUpdateInput,
-    ProgressUpdateResultUnion, ProviderLanguageInformation, ReviewPostedEvent,
-    SeenAnimeExtraInformation, SeenMangaExtraInformation, SeenPodcastExtraInformation,
-    SeenShowExtraInformation, ShowSpecifics, UpdateSeenItemInput, UserCalendarEventInput,
-    UserMediaNextEntry, UserMetadataDetailsEpisodeProgress, UserMetadataDetailsShowSeasonProgress,
-    UserUpcomingCalendarEventInput,
+    MetadataVideoSource, PartialMetadata, PartialMetadataWithoutId, PeopleListInput,
+    PeopleSearchInput, PeopleSearchItem, PersonAndMetadataGroupsSortBy, PersonDetailsGroupedByRole,
+    PersonDetailsItemWithCharacter, PodcastSpecifics, ProgressUpdateInput,
+    ProviderLanguageInformation, ReviewPostedEvent, SeenAnimeExtraInformation,
+    SeenPodcastExtraInformation, SeenShowExtraInformation, ShowSpecifics, UpdateSeenItemInput,
+    UserCalendarEventInput, UserMediaNextEntry, UserMetadataDetailsEpisodeProgress,
+    UserMetadataDetailsShowSeasonProgress, UserUpcomingCalendarEventInput,
 };
 use migrations::{
     AliasedMetadata, AliasedMetadataToGenre, AliasedReview, AliasedSeen, AliasedUserToEntity,
 };
-use moka::future::Cache;
 use nanoid::nanoid;
 use notification_service::send_notification;
 use providers::{
-    anilist::{AnilistAnimeService, AnilistMangaService, AnilistService, NonMediaAnilistService},
+    anilist::{AnilistService, NonMediaAnilistService},
     audible::AudibleService,
     google_books::GoogleBooksService,
     igdb::IgdbService,
     itunes::ITunesService,
     listennotes::ListennotesService,
-    mal::{MalAnimeService, MalMangaService, MalService, NonMediaMalService},
+    mal::{MalService, NonMediaMalService},
     manga_updates::MangaUpdatesService,
     openlibrary::OpenlibraryService,
-    tmdb::{NonMediaTmdbService, TmdbMovieService, TmdbService, TmdbShowService},
+    tmdb::TmdbService,
     vndb::VndbService,
 };
-use rust_decimal::prelude::{One, ToPrimitive};
 use rust_decimal::Decimal;
 use rust_decimal_macros::dec;
 use sea_orm::{
@@ -107,6 +104,7 @@ use sea_query::{
     PostgresQueryBuilder, Query, SelectStatement,
 };
 use serde::{Deserialize, Serialize};
+use supporting_service::SupportingService;
 use traits::{MediaProvider, MediaProviderLanguages, TraceOk};
 use user_models::UserReviewScale;
 use uuid::Uuid;
@@ -126,59 +124,7 @@ impl MediaProviderLanguages for CustomService {
     }
 }
 
-#[derive(Debug, Ord, PartialEq, Eq, PartialOrd, Clone, Hash)]
-struct CommitCache {
-    id: String,
-    lot: EntityLot,
-}
-
-pub struct MiscellaneousService {
-    is_pro: bool,
-    oidc_enabled: bool,
-    db: DatabaseConnection,
-    timezone: Arc<chrono_tz::Tz>,
-    config: Arc<config::AppConfig>,
-    cache_service: Arc<CacheService>,
-    commit_cache: Cache<CommitCache, ()>,
-    file_storage_service: Arc<FileStorageService>,
-    perform_application_job: MemoryStorage<ApplicationJob>,
-    perform_core_application_job: MemoryStorage<CoreApplicationJob>,
-}
-
-fn create_disk_cache<T: Eq + StdHash + Sync + Send + 'static>(hours: i64) -> Cache<T, ()> {
-    Cache::builder()
-        .time_to_live(ChronoDuration::try_hours(hours).unwrap().to_std().unwrap())
-        .build()
-}
-
-impl MiscellaneousService {
-    #[allow(clippy::too_many_arguments)]
-    pub async fn new(
-        is_pro: bool,
-        oidc_enabled: bool,
-        db: &DatabaseConnection,
-        timezone: Arc<chrono_tz::Tz>,
-        config: Arc<config::AppConfig>,
-        cache_service: Arc<CacheService>,
-        file_storage_service: Arc<FileStorageService>,
-        perform_application_job: &MemoryStorage<ApplicationJob>,
-        perform_core_application_job: &MemoryStorage<CoreApplicationJob>,
-    ) -> Self {
-        let commit_cache = create_disk_cache(1);
-        Self {
-            config,
-            is_pro,
-            timezone,
-            oidc_enabled,
-            commit_cache,
-            cache_service,
-            db: db.clone(),
-            file_storage_service,
-            perform_application_job: perform_application_job.clone(),
-            perform_core_application_job: perform_core_application_job.clone(),
-        }
-    }
-}
+pub struct MiscellaneousService(pub Arc<SupportingService>);
 
 impl MiscellaneousService {
     pub async fn update_claimed_recommendations_and_download_new_ones(&self) -> Result<()> {
@@ -191,13 +137,13 @@ impl MiscellaneousService {
             .column(metadata::Column::Id)
             .filter(metadata::Column::IsRecommendation.eq(true))
             .into_tuple::<String>()
-            .stream(&self.db)
+            .stream(&self.0.db)
             .await?;
         let mut recommendations_to_update = vec![];
         while let Some(meta) = metadata_stream.try_next().await? {
             let num_ute = UserToEntity::find()
                 .filter(user_to_entity::Column::MetadataId.eq(&meta))
-                .count(&self.db)
+                .count(&self.0.db)
                 .await?;
             if num_ute > 0 {
                 recommendations_to_update.push(meta);
@@ -209,7 +155,7 @@ impl MiscellaneousService {
                 is_recommendation: ActiveValue::Set(None),
                 ..Default::default()
             })
-            .exec(&self.db)
+            .exec(&self.0.db)
             .await?;
         ryot_log!(debug, "Downloading new recommendations for users");
         #[derive(Debug, FromQueryResult)]
@@ -235,11 +181,11 @@ ORDER BY RANDOM() LIMIT 10;
                 MediaSource::Vndb.into(),
             ],
         ))
-        .all(&self.db)
+        .all(&self.0.db)
         .await?;
         let mut media_item_ids = vec![];
         for media in media_items.into_iter() {
-            let provider = self.get_metadata_provider(media.lot, media.source).await?;
+            let provider = get_metadata_provider(media.lot, media.source, &self.0).await?;
             if let Ok(recommendations) = provider
                 .get_recommendations_for_metadata(&media.identifier)
                 .await
@@ -268,46 +214,43 @@ ORDER BY RANDOM() LIMIT 10;
                     .add(access_link::Column::ExpiresOn.lte(Utc::now())),
             )
             .into_tuple::<String>()
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         for access_link in access_links {
-            revoke_access_link(&self.db, access_link).await?;
+            revoke_access_link(&self.0.db, access_link).await?;
         }
         Ok(())
     }
 
     pub async fn core_details(&self) -> CoreDetails {
-        let mut files_enabled = self.config.file_storage.is_enabled();
-        if files_enabled && !self.file_storage_service.is_enabled().await {
+        let mut files_enabled = self.0.config.file_storage.is_enabled();
+        if files_enabled && !self.0.file_storage_service.is_enabled().await {
             files_enabled = false;
         }
         CoreDetails {
-            is_pro: self.is_pro,
+            is_pro: self.0.is_pro,
             version: APP_VERSION.to_owned(),
             file_storage_enabled: files_enabled,
-            oidc_enabled: self.oidc_enabled,
-            page_limit: self.config.frontend.page_size,
+            website_url: "https://ryot.io".to_owned(),
+            oidc_enabled: self.0.oidc_client.is_some(),
+            page_limit: self.0.config.frontend.page_size,
             docs_link: "https://docs.ryot.io".to_owned(),
             backend_errors: BackendError::iter().collect(),
-            smtp_enabled: self.config.server.smtp.is_enabled(),
-            website_url: "https://ryot.io".to_owned(),
-            signup_allowed: self.config.users.allow_registration,
-            local_auth_disabled: self.config.users.disable_local_auth,
-            token_valid_for_days: self.config.users.token_valid_for_days,
+            smtp_enabled: self.0.config.server.smtp.is_enabled(),
+            signup_allowed: self.0.config.users.allow_registration,
+            local_auth_disabled: self.0.config.users.disable_local_auth,
+            token_valid_for_days: self.0.config.users.token_valid_for_days,
             repository_link: "https://github.com/ignisda/ryot".to_owned(),
         }
     }
 
-    fn get_integration_service(&self) -> IntegrationService {
-        IntegrationService::new(&self.db)
-    }
-
     async fn metadata_assets(&self, meta: &metadata::Model) -> Result<GraphqlMediaAssets> {
-        let images = metadata_images_as_urls(&meta.images, &self.file_storage_service).await;
+        let images = metadata_images_as_urls(&meta.images, &self.0.file_storage_service).await;
         let mut videos = vec![];
         if let Some(vids) = &meta.videos {
             for v in vids.clone() {
                 let url = self
+                    .0
                     .file_storage_service
                     .get_stored_asset(v.identifier)
                     .await;
@@ -322,7 +265,7 @@ ORDER BY RANDOM() LIMIT 10;
 
     async fn generic_metadata(&self, metadata_id: &String) -> Result<MetadataBaseData> {
         let mut meta = match Metadata::find_by_id(metadata_id)
-            .one(&self.db)
+            .one(&self.0.db)
             .await
             .unwrap()
         {
@@ -333,7 +276,7 @@ ORDER BY RANDOM() LIMIT 10;
             .find_related(Genre)
             .order_by_asc(genre::Column::Name)
             .into_model::<GenreListItem>()
-            .all(&self.db)
+            .all(&self.0.db)
             .await
             .unwrap();
         #[derive(Debug, FromQueryResult)]
@@ -360,11 +303,11 @@ ORDER BY RANDOM() LIMIT 10;
             )
             .order_by_asc(metadata_to_person::Column::Index)
             .into_model::<PartialCreator>()
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         let mut creators: HashMap<String, Vec<_>> = HashMap::new();
         for cr in crts {
-            let image = first_metadata_image_as_url(&cr.images, &self.file_storage_service).await;
+            let image = first_metadata_image_as_url(&cr.images, &self.0.file_storage_service).await;
             let creator = MetadataCreator {
                 image,
                 name: cr.name,
@@ -421,7 +364,7 @@ ORDER BY RANDOM() LIMIT 10;
                 metadata_to_metadata::Column::Relation.eq(MetadataToMetadataRelation::Suggestion),
             )
             .into_tuple::<String>()
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         let assets = self.metadata_assets(&meta).await.unwrap();
         Ok(MetadataBaseData {
@@ -447,15 +390,23 @@ ORDER BY RANDOM() LIMIT 10;
                 metadata::Column::PublishYear,
             ])
             .into_model::<MetadataPartialDetails>()
-            .one(&self.db)
+            .one(&self.0.db)
             .await
             .unwrap()
             .ok_or_else(|| Error::new("The record does not exist".to_owned()))?;
         metadata.image =
-            first_metadata_image_as_url(&metadata.images, &self.file_storage_service).await;
+            first_metadata_image_as_url(&metadata.images, &self.0.file_storage_service).await;
         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.perform_application_job).await
+    }
+
     pub async fn metadata_details(&self, metadata_id: &String) -> Result<GraphqlMetadataDetails> {
         let MetadataBaseData {
             model,
@@ -516,12 +467,16 @@ ORDER BY RANDOM() LIMIT 10;
         let group = {
             let association = MetadataToMetadataGroup::find()
                 .filter(metadata_to_metadata_group::Column::MetadataId.eq(metadata_id))
-                .one(&self.db)
+                .one(&self.0.db)
                 .await?;
             match association {
                 None => None,
                 Some(a) => {
-                    let grp = a.find_related(MetadataGroup).one(&self.db).await?.unwrap();
+                    let grp = a
+                        .find_related(MetadataGroup)
+                        .one(&self.0.db)
+                        .await?
+                        .unwrap();
                     Some(GraphqlMetadataGroup {
                         id: grp.id,
                         name: grp.title,
@@ -573,12 +528,16 @@ ORDER BY RANDOM() LIMIT 10;
     ) -> Result<UserMetadataDetails> {
         let media_details = self.generic_metadata(&metadata_id).await?;
         let collections =
-            entity_in_collections(&self.db, &user_id, &metadata_id, EntityLot::Metadata).await?;
-        let reviews =
-            item_reviews(&self.db, &user_id, &metadata_id, EntityLot::Metadata, true).await?;
-        let (_, history) = self
-            .is_metadata_finished_by_user(&user_id, &media_details)
-            .await?;
+            entity_in_collections(&self.0.db, &user_id, &metadata_id, EntityLot::Metadata).await?;
+        let reviews = item_reviews(
+            &self.0.db,
+            &user_id,
+            &metadata_id,
+            EntityLot::Metadata,
+            true,
+        )
+        .await?;
+        let (_, history) = is_metadata_finished_by_user(&user_id, &metadata_id, &self.0.db).await?;
         let in_progress = history
             .iter()
             .find(|h| h.state == SeenState::InProgress || h.state == SeenState::OnAHold)
@@ -664,6 +623,7 @@ ORDER BY RANDOM() LIMIT 10;
             .to_owned();
         let stmt = self.get_db_stmt(seen_select);
         let seen_by = self
+            .0
             .db
             .query_one(stmt)
             .await?
@@ -671,7 +631,7 @@ ORDER BY RANDOM() LIMIT 10;
             .unwrap();
         let seen_by: usize = seen_by.try_into().unwrap();
         let user_to_meta =
-            get_user_to_entity_association(&self.db, &user_id, metadata_id, EntityLot::Metadata)
+            get_user_to_entity_association(&self.0.db, &user_id, metadata_id, EntityLot::Metadata)
                 .await;
         let average_rating = if reviews.is_empty() {
             None
@@ -761,9 +721,10 @@ ORDER BY RANDOM() LIMIT 10;
         user_id: String,
         person_id: String,
     ) -> Result<UserPersonDetails> {
-        let reviews = item_reviews(&self.db, &user_id, &person_id, EntityLot::Person, true).await?;
+        let reviews =
+            item_reviews(&self.0.db, &user_id, &person_id, EntityLot::Person, true).await?;
         let collections =
-            entity_in_collections(&self.db, &user_id, &person_id, EntityLot::Person).await?;
+            entity_in_collections(&self.0.db, &user_id, &person_id, EntityLot::Person).await?;
         Ok(UserPersonDetails {
             reviews,
             collections,
@@ -776,14 +737,14 @@ ORDER BY RANDOM() LIMIT 10;
         metadata_group_id: String,
     ) -> Result<UserMetadataGroupDetails> {
         let collections = entity_in_collections(
-            &self.db,
+            &self.0.db,
             &user_id,
             &metadata_group_id,
             EntityLot::MetadataGroup,
         )
         .await?;
         let reviews = item_reviews(
-            &self.db,
+            &self.0.db,
             &user_id,
             &metadata_group_id,
             EntityLot::MetadataGroup,
@@ -867,7 +828,7 @@ ORDER BY RANDOM() LIMIT 10;
             })
             .limit(media_limit)
             .into_model::<CalEvent>()
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         let mut events = vec![];
         for evt in all_events {
@@ -903,7 +864,8 @@ ORDER BY RANDOM() LIMIT 10;
             };
 
             if image.is_none() {
-                image = first_metadata_image_as_url(&evt.m_images, &self.file_storage_service).await
+                image =
+                    first_metadata_image_as_url(&evt.m_images, &self.0.file_storage_service).await
             }
             calc.metadata_image = image;
             calc.episode_name = title;
@@ -951,27 +913,12 @@ ORDER BY RANDOM() LIMIT 10;
         Ok(events)
     }
 
-    async fn seen_history(
-        &self,
-        user_id: &String,
-        metadata_id: &String,
-    ) -> Result<Vec<seen::Model>> {
-        let seen_items = Seen::find()
-            .filter(seen::Column::UserId.eq(user_id))
-            .filter(seen::Column::MetadataId.eq(metadata_id))
-            .order_by_desc(seen::Column::LastUpdatedOn)
-            .all(&self.db)
-            .await
-            .unwrap();
-        Ok(seen_items)
-    }
-
     pub async fn metadata_list(
         &self,
         user_id: String,
         input: MetadataListInput,
     ) -> Result<SearchResults<String>> {
-        let preferences = user_preferences_by_id(&self.db, &user_id, &self.config).await?;
+        let preferences = user_preferences_by_id(&user_id, &self.0).await?;
 
         let avg_rating_col = "user_average_rating";
         let cloned_user_id_1 = user_id.clone();
@@ -986,7 +933,9 @@ ORDER BY RANDOM() LIMIT 10;
             UserReviewScale::OutOfFive => 20,
             UserReviewScale::OutOfHundred | UserReviewScale::ThreePointSmiley => 1,
         };
-        let take = input.take.unwrap_or(self.config.frontend.page_size as u64);
+        let take = input
+            .take
+            .unwrap_or(self.0.config.frontend.page_size as u64);
         let page: u64 = input
             .search
             .clone()
@@ -1096,7 +1045,7 @@ ORDER BY RANDOM() LIMIT 10;
                 ),
             })
             .into_tuple::<String>()
-            .paginate(&self.db, take);
+            .paginate(&self.0.db, take);
         let ItemsAndPagesNumber {
             number_of_items,
             number_of_pages,
@@ -1118,247 +1067,13 @@ ORDER BY RANDOM() LIMIT 10;
         })
     }
 
-    pub async fn progress_update(
-        &self,
-        input: ProgressUpdateInput,
-        user_id: &String,
-        // DEV: update only if media has not been consumed for this user in the last `n` duration
-        respect_cache: bool,
-    ) -> Result<ProgressUpdateResultUnion> {
-        let cache = ApplicationCacheKey::ProgressUpdateCache {
-            user_id: user_id.to_owned(),
-            metadata_id: input.metadata_id.clone(),
-            show_season_number: input.show_season_number,
-            show_episode_number: input.show_episode_number,
-            manga_volume_number: input.manga_volume_number,
-            manga_chapter_number: input.manga_chapter_number,
-            anime_episode_number: input.anime_episode_number,
-            podcast_episode_number: input.podcast_episode_number,
-        };
-        let in_cache = self.cache_service.get(cache.clone()).await?;
-        if respect_cache && in_cache.is_some() {
-            ryot_log!(debug, "Seen is already in cache");
-            return Ok(ProgressUpdateResultUnion::Error(ProgressUpdateError {
-                error: ProgressUpdateErrorVariant::AlreadySeen,
-            }));
-        }
-        ryot_log!(debug, "Input for progress_update = {:?}", input);
-
-        let all_prev_seen = Seen::find()
-            .filter(seen::Column::Progress.lt(100))
-            .filter(seen::Column::UserId.eq(user_id))
-            .filter(seen::Column::State.ne(SeenState::Dropped))
-            .filter(seen::Column::MetadataId.eq(&input.metadata_id))
-            .order_by_desc(seen::Column::LastUpdatedOn)
-            .all(&self.db)
-            .await
-            .unwrap();
-        #[derive(Debug, Serialize, Deserialize, Enum, Clone, PartialEq, Eq, Copy)]
-        enum ProgressUpdateAction {
-            Update,
-            Now,
-            InThePast,
-            JustStarted,
-            ChangeState,
-        }
-        let action = match input.change_state {
-            None => match input.progress {
-                None => ProgressUpdateAction::ChangeState,
-                Some(p) => {
-                    if p == dec!(100) {
-                        match input.date {
-                            None => ProgressUpdateAction::InThePast,
-                            Some(u) => {
-                                if get_current_date(&self.timezone) == u {
-                                    if all_prev_seen.is_empty() {
-                                        ProgressUpdateAction::Now
-                                    } else {
-                                        ProgressUpdateAction::Update
-                                    }
-                                } else {
-                                    ProgressUpdateAction::InThePast
-                                }
-                            }
-                        }
-                    } else if all_prev_seen.is_empty() {
-                        ProgressUpdateAction::JustStarted
-                    } else {
-                        ProgressUpdateAction::Update
-                    }
-                }
-            },
-            Some(_) => ProgressUpdateAction::ChangeState,
-        };
-        ryot_log!(debug, "Progress update action = {:?}", action);
-        let err = || {
-            Ok(ProgressUpdateResultUnion::Error(ProgressUpdateError {
-                error: ProgressUpdateErrorVariant::NoSeenInProgress,
-            }))
-        };
-        let seen = match action {
-            ProgressUpdateAction::Update => {
-                let prev_seen = all_prev_seen[0].clone();
-                let progress = input.progress.unwrap();
-                let watched_on = prev_seen.provider_watched_on.clone();
-                if prev_seen.progress == progress && watched_on == input.provider_watched_on {
-                    ryot_log!(debug, "No progress update required");
-                    return Ok(ProgressUpdateResultUnion::Error(ProgressUpdateError {
-                        error: ProgressUpdateErrorVariant::UpdateWithoutProgressUpdate,
-                    }));
-                }
-                let mut updated_at = prev_seen.updated_at.clone();
-                let now = Utc::now();
-                if prev_seen.progress != progress {
-                    updated_at.push(now);
-                }
-                let mut last_seen: seen::ActiveModel = prev_seen.into();
-                last_seen.state = ActiveValue::Set(SeenState::InProgress);
-                last_seen.progress = ActiveValue::Set(progress);
-                last_seen.updated_at = ActiveValue::Set(updated_at);
-                last_seen.provider_watched_on =
-                    ActiveValue::Set(input.provider_watched_on.or(watched_on));
-                if progress == dec!(100) {
-                    last_seen.finished_on = ActiveValue::Set(Some(now.date_naive()));
-                }
-
-                // This is needed for manga as some of the apps will update in weird orders
-                // For example with komga mihon will update out of order to the server
-                if input.manga_chapter_number.is_some() {
-                    last_seen.manga_extra_information =
-                        ActiveValue::set(Some(SeenMangaExtraInformation {
-                            chapter: input.manga_chapter_number,
-                            volume: input.manga_volume_number,
-                        }))
-                }
-
-                last_seen.update(&self.db).await.unwrap()
-            }
-            ProgressUpdateAction::ChangeState => {
-                let new_state = input.change_state.unwrap_or(SeenState::Dropped);
-                let last_seen = Seen::find()
-                    .filter(seen::Column::UserId.eq(user_id))
-                    .filter(seen::Column::MetadataId.eq(input.metadata_id))
-                    .order_by_desc(seen::Column::LastUpdatedOn)
-                    .one(&self.db)
-                    .await
-                    .unwrap();
-                match last_seen {
-                    Some(ls) => {
-                        let watched_on = ls.provider_watched_on.clone();
-                        let mut updated_at = ls.updated_at.clone();
-                        let now = Utc::now();
-                        updated_at.push(now);
-                        let mut last_seen: seen::ActiveModel = ls.into();
-                        last_seen.state = ActiveValue::Set(new_state);
-                        last_seen.updated_at = ActiveValue::Set(updated_at);
-                        last_seen.provider_watched_on =
-                            ActiveValue::Set(input.provider_watched_on.or(watched_on));
-                        last_seen.update(&self.db).await.unwrap()
-                    }
-                    None => {
-                        return err();
-                    }
-                }
-            }
-            ProgressUpdateAction::Now
-            | ProgressUpdateAction::InThePast
-            | ProgressUpdateAction::JustStarted => {
-                let meta = Metadata::find_by_id(&input.metadata_id)
-                    .one(&self.db)
-                    .await
-                    .unwrap()
-                    .unwrap();
-                ryot_log!(
-                    debug,
-                    "Progress update for meta {:?} ({:?})",
-                    meta.title,
-                    meta.lot
-                );
-
-                let show_ei = if matches!(meta.lot, MediaLot::Show) {
-                    let season = input.show_season_number.ok_or_else(|| {
-                        Error::new("Season number is required for show progress update")
-                    })?;
-                    let episode = input.show_episode_number.ok_or_else(|| {
-                        Error::new("Episode number is required for show progress update")
-                    })?;
-                    Some(SeenShowExtraInformation { season, episode })
-                } else {
-                    None
-                };
-                let podcast_ei = if matches!(meta.lot, MediaLot::Podcast) {
-                    let episode = input.podcast_episode_number.ok_or_else(|| {
-                        Error::new("Episode number is required for podcast progress update")
-                    })?;
-                    Some(SeenPodcastExtraInformation { episode })
-                } else {
-                    None
-                };
-                let anime_ei = if matches!(meta.lot, MediaLot::Anime) {
-                    Some(SeenAnimeExtraInformation {
-                        episode: input.anime_episode_number,
-                    })
-                } else {
-                    None
-                };
-                let manga_ei = if matches!(meta.lot, MediaLot::Manga) {
-                    Some(SeenMangaExtraInformation {
-                        chapter: input.manga_chapter_number,
-                        volume: input.manga_volume_number,
-                    })
-                } else {
-                    None
-                };
-                let finished_on = if action == ProgressUpdateAction::JustStarted {
-                    None
-                } else {
-                    input.date
-                };
-                ryot_log!(debug, "Progress update finished on = {:?}", finished_on);
-                let (progress, started_on) = if matches!(action, ProgressUpdateAction::JustStarted)
-                {
-                    (
-                        input.progress.unwrap_or(dec!(0)),
-                        Some(Utc::now().date_naive()),
-                    )
-                } else {
-                    (dec!(100), None)
-                };
-                ryot_log!(debug, "Progress update percentage = {:?}", progress);
-                let seen_insert = seen::ActiveModel {
-                    progress: ActiveValue::Set(progress),
-                    user_id: ActiveValue::Set(user_id.to_owned()),
-                    metadata_id: ActiveValue::Set(input.metadata_id),
-                    started_on: ActiveValue::Set(started_on),
-                    finished_on: ActiveValue::Set(finished_on),
-                    state: ActiveValue::Set(SeenState::InProgress),
-                    provider_watched_on: ActiveValue::Set(input.provider_watched_on),
-                    show_extra_information: ActiveValue::Set(show_ei),
-                    podcast_extra_information: ActiveValue::Set(podcast_ei),
-                    anime_extra_information: ActiveValue::Set(anime_ei),
-                    manga_extra_information: ActiveValue::Set(manga_ei),
-                    ..Default::default()
-                };
-                seen_insert.insert(&self.db).await.unwrap()
-            }
-        };
-        ryot_log!(debug, "Progress update = {:?}", seen);
-        let id = seen.id.clone();
-        if seen.state == SeenState::Completed && respect_cache {
-            self.cache_service
-                .set_with_expiry(cache, self.config.server.progress_update_threshold)
-                .await?;
-        }
-        self.after_media_seen_tasks(seen).await?;
-        Ok(ProgressUpdateResultUnion::Ok(StringIdObject { id }))
-    }
-
     pub async fn deploy_bulk_progress_update(
         &self,
         user_id: String,
         input: Vec<ProgressUpdateInput>,
     ) -> Result<bool> {
-        self.perform_core_application_job
+        self.0
+            .perform_core_application_job
             .clone()
             .enqueue(CoreApplicationJob::BulkProgressUpdate(user_id, input))
             .await
@@ -1372,7 +1087,9 @@ ORDER BY RANDOM() LIMIT 10;
         input: Vec<ProgressUpdateInput>,
     ) -> Result<bool> {
         for seen in input {
-            self.progress_update(seen, &user_id, false).await.trace_ok();
+            progress_update(seen, &user_id, false, &self.0)
+                .await
+                .trace_ok();
         }
         Ok(true)
     }
@@ -1382,69 +1099,7 @@ ORDER BY RANDOM() LIMIT 10;
         user_id: &String,
         job_name: BackgroundJob,
     ) -> Result<bool> {
-        let core_storage = &mut self.perform_core_application_job.clone();
-        let storage = &mut self.perform_application_job.clone();
-        match job_name {
-            BackgroundJob::UpdateAllMetadata
-            | BackgroundJob::UpdateAllExercises
-            | BackgroundJob::RecalculateCalendarEvents
-            | BackgroundJob::PerformBackgroundTasks => {
-                admin_account_guard(&self.db, user_id).await?;
-            }
-            _ => {}
-        }
-        match job_name {
-            BackgroundJob::UpdateAllMetadata => {
-                let many_metadata = Metadata::find()
-                    .select_only()
-                    .column(metadata::Column::Id)
-                    .order_by_asc(metadata::Column::LastUpdatedOn)
-                    .into_tuple::<String>()
-                    .all(&self.db)
-                    .await
-                    .unwrap();
-                for metadata_id in many_metadata {
-                    self.deploy_update_metadata_job(&metadata_id, true).await?;
-                }
-            }
-            BackgroundJob::UpdateAllExercises => {
-                self.perform_application_job
-                    .enqueue(ApplicationJob::UpdateExerciseLibrary)
-                    .await
-                    .unwrap();
-            }
-            BackgroundJob::RecalculateCalendarEvents => {
-                storage
-                    .enqueue(ApplicationJob::RecalculateCalendarEvents)
-                    .await
-                    .unwrap();
-            }
-            BackgroundJob::PerformBackgroundTasks => {
-                storage
-                    .enqueue(ApplicationJob::PerformBackgroundTasks)
-                    .await
-                    .unwrap();
-            }
-            BackgroundJob::SyncIntegrationsData => {
-                core_storage
-                    .enqueue(CoreApplicationJob::SyncIntegrationsData(user_id.to_owned()))
-                    .await
-                    .unwrap();
-            }
-            BackgroundJob::CalculateUserActivitiesAndSummary => {
-                storage
-                    .enqueue(ApplicationJob::RecalculateUserActivitiesAndSummary(
-                        user_id.to_owned(),
-                        true,
-                    ))
-                    .await
-                    .unwrap();
-            }
-            BackgroundJob::ReEvaluateUserWorkouts => {
-                deploy_job_to_re_evaluate_user_workouts(storage, user_id).await;
-            }
-        };
-        Ok(true)
+        deploy_background_job(user_id, job_name, &self.0).await
     }
 
     async fn cleanup_user_and_metadata_association(&self) -> Result<()> {
@@ -1452,7 +1107,7 @@ ORDER BY RANDOM() LIMIT 10;
             .select_only()
             .column(user::Column::Id)
             .into_tuple::<String>()
-            .all(&self.db)
+            .all(&self.0.db)
             .await
             .unwrap();
         for user_id in all_users {
@@ -1461,7 +1116,7 @@ ORDER BY RANDOM() LIMIT 10;
                 .column(collection::Column::UserId)
                 .left_join(UserToEntity)
                 .filter(user_to_entity::Column::UserId.eq(&user_id))
-                .all(&self.db)
+                .all(&self.0.db)
                 .await
                 .unwrap();
             let monitoring_collection_id = collections
@@ -1493,16 +1148,15 @@ ORDER BY RANDOM() LIMIT 10;
             let all_user_to_entities = UserToEntity::find()
                 .filter(user_to_entity::Column::NeedsToBeUpdated.eq(true))
                 .filter(user_to_entity::Column::UserId.eq(&user_id))
-                .all(&self.db)
+                .all(&self.0.db)
                 .await
                 .unwrap();
             for ute in all_user_to_entities {
                 let mut new_reasons = HashSet::new();
                 let (entity_id, entity_lot) = if let Some(metadata_id) = ute.metadata_id.clone() {
-                    let metadata = self.generic_metadata(&metadata_id).await?;
-                    let (is_finished, seen_history) = self
-                        .is_metadata_finished_by_user(&ute.user_id, &metadata)
-                        .await?;
+                    let (is_finished, seen_history) =
+                        is_metadata_finished_by_user(&ute.user_id, &metadata_id, &self.0.db)
+                            .await?;
                     if !seen_history.is_empty() {
                         new_reasons.insert(UserToMediaReason::Seen);
                     }
@@ -1520,7 +1174,7 @@ ORDER BY RANDOM() LIMIT 10;
                 };
 
                 let collections_part_of =
-                    entity_in_collections(&self.db, &user_id, &entity_id, entity_lot)
+                    entity_in_collections(&self.0.db, &user_id, &entity_id, entity_lot)
                         .await?
                         .into_iter()
                         .map(|c| c.id)
@@ -1533,7 +1187,7 @@ ORDER BY RANDOM() LIMIT 10;
                             .or(review::Column::MetadataGroupId.eq(ute.metadata_group_id.clone()))
                             .or(review::Column::PersonId.eq(ute.person_id.clone())),
                     )
-                    .count(&self.db)
+                    .count(&self.0.db)
                     .await
                     .unwrap()
                     > 0
@@ -1564,7 +1218,7 @@ ORDER BY RANDOM() LIMIT 10;
                     HashSet::from_iter(ute.media_reason.clone().unwrap_or_default().into_iter());
                 if new_reasons.is_empty() {
                     ryot_log!(debug, "Deleting user_to_entity = {id:?}", id = (&ute.id));
-                    ute.delete(&self.db).await.unwrap();
+                    ute.delete(&self.0.db).await.unwrap();
                 } else {
                     let mut ute: user_to_entity::ActiveModel = ute.into();
                     if new_reasons != previous_reasons {
@@ -1573,271 +1227,13 @@ ORDER BY RANDOM() LIMIT 10;
                             ActiveValue::Set(Some(new_reasons.into_iter().collect()));
                     }
                     ute.needs_to_be_updated = ActiveValue::Set(None);
-                    ute.update(&self.db).await.unwrap();
+                    ute.update(&self.0.db).await.unwrap();
                 }
             }
         }
         Ok(())
     }
 
-    async fn update_media(
-        &self,
-        metadata_id: &String,
-        input: MediaDetails,
-    ) -> Result<Vec<(String, MediaStateChanged)>> {
-        let mut notifications = vec![];
-
-        let meta = Metadata::find_by_id(metadata_id)
-            .one(&self.db)
-            .await
-            .unwrap()
-            .unwrap();
-
-        if let (Some(p1), Some(p2)) = (&meta.production_status, &input.production_status) {
-            if p1 != p2 {
-                notifications.push((
-                    format!("Status changed from {:#?} to {:#?}", p1, p2),
-                    MediaStateChanged::MetadataStatusChanged,
-                ));
-            }
-        }
-        if let (Some(p1), Some(p2)) = (meta.publish_year, input.publish_year) {
-            if p1 != p2 {
-                notifications.push((
-                    format!("Publish year from {:#?} to {:#?}", p1, p2),
-                    MediaStateChanged::MetadataReleaseDateChanged,
-                ));
-            }
-        }
-        if let (Some(s1), Some(s2)) = (&meta.show_specifics, &input.show_specifics) {
-            if s1.seasons.len() != s2.seasons.len() {
-                notifications.push((
-                    format!(
-                        "Number of seasons changed from {:#?} to {:#?}",
-                        s1.seasons.len(),
-                        s2.seasons.len()
-                    ),
-                    MediaStateChanged::MetadataNumberOfSeasonsChanged,
-                ));
-            } else {
-                for (s1, s2) in zip(s1.seasons.iter(), s2.seasons.iter()) {
-                    if SHOW_SPECIAL_SEASON_NAMES.contains(&s1.name.as_str())
-                        && SHOW_SPECIAL_SEASON_NAMES.contains(&s2.name.as_str())
-                    {
-                        continue;
-                    }
-                    if s1.episodes.len() != s2.episodes.len() {
-                        notifications.push((
-                            format!(
-                                "Number of episodes changed from {:#?} to {:#?} (Season {})",
-                                s1.episodes.len(),
-                                s2.episodes.len(),
-                                s1.season_number
-                            ),
-                            MediaStateChanged::MetadataEpisodeReleased,
-                        ));
-                    } else {
-                        for (before_episode, after_episode) in
-                            zip(s1.episodes.iter(), s2.episodes.iter())
-                        {
-                            if before_episode.name != after_episode.name {
-                                notifications.push((
-                                    format!(
-                                        "Episode name changed from {:#?} to {:#?} (S{}E{})",
-                                        before_episode.name,
-                                        after_episode.name,
-                                        s1.season_number,
-                                        before_episode.episode_number
-                                    ),
-                                    MediaStateChanged::MetadataEpisodeNameChanged,
-                                ));
-                            }
-                            if before_episode.poster_images != after_episode.poster_images {
-                                notifications.push((
-                                    format!(
-                                        "Episode image changed for S{}E{}",
-                                        s1.season_number, before_episode.episode_number
-                                    ),
-                                    MediaStateChanged::MetadataEpisodeImagesChanged,
-                                ));
-                            }
-                            if let (Some(pd1), Some(pd2)) =
-                                (before_episode.publish_date, after_episode.publish_date)
-                            {
-                                if pd1 != pd2 {
-                                    notifications.push((
-                                            format!(
-                                                "Episode release date changed from {:?} to {:?} (S{}E{})",
-                                                pd1,
-                                                pd2,
-                                                s1.season_number,
-                                                before_episode.episode_number
-                                            ),
-                                            MediaStateChanged::MetadataReleaseDateChanged,
-                                        ));
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        };
-        if let (Some(a1), Some(a2)) = (&meta.anime_specifics, &input.anime_specifics) {
-            if let (Some(e1), Some(e2)) = (a1.episodes, a2.episodes) {
-                if e1 != e2 {
-                    notifications.push((
-                        format!("Number of episodes changed from {:#?} to {:#?}", e1, e2),
-                        MediaStateChanged::MetadataChaptersOrEpisodesChanged,
-                    ));
-                }
-            }
-        };
-        if let (Some(m1), Some(m2)) = (&meta.manga_specifics, &input.manga_specifics) {
-            if let (Some(c1), Some(c2)) = (m1.chapters, m2.chapters) {
-                if c1 != c2 {
-                    notifications.push((
-                        format!("Number of chapters changed from {:#?} to {:#?}", c1, c2),
-                        MediaStateChanged::MetadataChaptersOrEpisodesChanged,
-                    ));
-                }
-            }
-        };
-        if let (Some(p1), Some(p2)) = (&meta.podcast_specifics, &input.podcast_specifics) {
-            if p1.episodes.len() != p2.episodes.len() {
-                notifications.push((
-                    format!(
-                        "Number of episodes changed from {:#?} to {:#?}",
-                        p1.episodes.len(),
-                        p2.episodes.len()
-                    ),
-                    MediaStateChanged::MetadataEpisodeReleased,
-                ));
-            } else {
-                for (before_episode, after_episode) in zip(p1.episodes.iter(), p2.episodes.iter()) {
-                    if before_episode.title != after_episode.title {
-                        notifications.push((
-                            format!(
-                                "Episode name changed from {:#?} to {:#?} (EP{})",
-                                before_episode.title, after_episode.title, before_episode.number
-                            ),
-                            MediaStateChanged::MetadataEpisodeNameChanged,
-                        ));
-                    }
-                    if before_episode.thumbnail != after_episode.thumbnail {
-                        notifications.push((
-                            format!("Episode image changed for EP{}", before_episode.number),
-                            MediaStateChanged::MetadataEpisodeImagesChanged,
-                        ));
-                    }
-                }
-            }
-        };
-
-        let notifications = notifications
-            .into_iter()
-            .map(|n| (format!("{} for {:?}.", n.0, meta.title), n.1))
-            .collect_vec();
-
-        let mut images = vec![];
-        images.extend(input.url_images.into_iter().map(|i| MetadataImage {
-            url: StoredUrl::Url(i.image),
-        }));
-        images.extend(input.s3_images.into_iter().map(|i| MetadataImage {
-            url: StoredUrl::S3(i.image),
-        }));
-        let free_creators = if input.creators.is_empty() {
-            None
-        } else {
-            Some(input.creators)
-        };
-        let watch_providers = if input.watch_providers.is_empty() {
-            None
-        } else {
-            Some(input.watch_providers)
-        };
-
-        let mut meta: metadata::ActiveModel = meta.into();
-        meta.last_updated_on = ActiveValue::Set(Utc::now());
-        meta.title = ActiveValue::Set(input.title);
-        meta.is_nsfw = ActiveValue::Set(input.is_nsfw);
-        meta.is_partial = ActiveValue::Set(Some(false));
-        meta.provider_rating = ActiveValue::Set(input.provider_rating);
-        meta.description = ActiveValue::Set(input.description);
-        meta.images = ActiveValue::Set(Some(images));
-        meta.videos = ActiveValue::Set(Some(input.videos));
-        meta.production_status = ActiveValue::Set(input.production_status);
-        meta.original_language = ActiveValue::Set(input.original_language);
-        meta.publish_year = ActiveValue::Set(input.publish_year);
-        meta.publish_date = ActiveValue::Set(input.publish_date);
-        meta.free_creators = ActiveValue::Set(free_creators);
-        meta.watch_providers = ActiveValue::Set(watch_providers);
-        meta.anime_specifics = ActiveValue::Set(input.anime_specifics);
-        meta.audio_book_specifics = ActiveValue::Set(input.audio_book_specifics);
-        meta.manga_specifics = ActiveValue::Set(input.manga_specifics);
-        meta.movie_specifics = ActiveValue::Set(input.movie_specifics);
-        meta.podcast_specifics = ActiveValue::Set(input.podcast_specifics);
-        meta.show_specifics = ActiveValue::Set(input.show_specifics);
-        meta.book_specifics = ActiveValue::Set(input.book_specifics);
-        meta.video_game_specifics = ActiveValue::Set(input.video_game_specifics);
-        meta.visual_novel_specifics = ActiveValue::Set(input.visual_novel_specifics);
-        meta.external_identifiers = ActiveValue::Set(input.external_identifiers);
-        let metadata = meta.update(&self.db).await.unwrap();
-
-        self.change_metadata_associations(
-            &metadata.id,
-            metadata.lot,
-            metadata.source,
-            input.genres,
-            input.suggestions,
-            input.group_identifiers,
-            input.people,
-        )
-        .await?;
-        Ok(notifications)
-    }
-
-    async fn associate_person_with_metadata(
-        &self,
-        metadata_id: &str,
-        person: PartialMetadataPerson,
-        index: usize,
-    ) -> Result<()> {
-        let role = person.role.clone();
-        let db_person = self
-            .commit_person(CommitPersonInput {
-                identifier: person.identifier.clone(),
-                source: person.source,
-                source_specifics: person.source_specifics,
-                name: person.name,
-            })
-            .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())),
-            character: ActiveValue::Set(person.character),
-        };
-        intermediate.insert(&self.db).await.ok();
-        Ok(())
-    }
-
-    async fn deploy_associate_group_with_metadata_job(
-        &self,
-        lot: MediaLot,
-        source: MediaSource,
-        identifier: String,
-    ) -> Result<()> {
-        self.perform_application_job
-            .clone()
-            .enqueue(ApplicationJob::AssociateGroupWithMetadata(
-                lot, source, identifier,
-            ))
-            .await
-            .unwrap();
-        Ok(())
-    }
-
     pub async fn commit_metadata_group_internal(
         &self,
         identifier: &String,
@@ -1848,108 +1244,33 @@ ORDER BY RANDOM() LIMIT 10;
             .filter(metadata_group::Column::Identifier.eq(identifier))
             .filter(metadata_group::Column::Lot.eq(lot))
             .filter(metadata_group::Column::Source.eq(source))
-            .one(&self.db)
+            .one(&self.0.db)
             .await?;
-        let provider = self.get_metadata_provider(lot, source).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.db).await?;
+                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.db).await?;
+                let new_group = db_group.insert(&self.0.db).await?;
                 new_group.id
             }
         };
         Ok((group_id, associated_items))
     }
 
-    async fn associate_suggestion_with_metadata(
-        &self,
-        data: PartialMetadataWithoutId,
-        metadata_id: &str,
-    ) -> Result<()> {
-        let db_partial_metadata = self.create_partial_metadata(data).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),
-            relation: ActiveValue::Set(MetadataToMetadataRelation::Suggestion),
-            ..Default::default()
-        };
-        intermediate.insert(&self.db).await.ok();
-        Ok(())
-    }
-
     async fn create_partial_metadata(
         &self,
         data: PartialMetadataWithoutId,
     ) -> Result<PartialMetadata> {
-        let mode = if let Some(c) = Metadata::find()
-            .filter(metadata::Column::Identifier.eq(&data.identifier))
-            .filter(metadata::Column::Lot.eq(data.lot))
-            .filter(metadata::Column::Source.eq(data.source))
-            .one(&self.db)
-            .await
-            .unwrap()
-        {
-            c
-        } else {
-            let image = data.image.clone().map(|i| {
-                vec![MetadataImage {
-                    url: StoredUrl::Url(i),
-                }]
-            });
-            let c = metadata::ActiveModel {
-                title: ActiveValue::Set(data.title),
-                identifier: ActiveValue::Set(data.identifier),
-                lot: ActiveValue::Set(data.lot),
-                source: ActiveValue::Set(data.source),
-                images: ActiveValue::Set(image),
-                is_partial: ActiveValue::Set(Some(true)),
-                is_recommendation: ActiveValue::Set(data.is_recommendation),
-                ..Default::default()
-            };
-            c.insert(&self.db).await?
-        };
-        let model = PartialMetadata {
-            id: mode.id,
-            title: mode.title,
-            identifier: mode.identifier,
-            lot: mode.lot,
-            source: mode.source,
-            image: data.image,
-            is_recommendation: mode.is_recommendation,
-        };
-        Ok(model)
-    }
-
-    async fn associate_genre_with_metadata(&self, name: String, metadata_id: &str) -> Result<()> {
-        let db_genre = if let Some(c) = Genre::find()
-            .filter(genre::Column::Name.eq(&name))
-            .one(&self.db)
-            .await
-            .unwrap()
-        {
-            c
-        } else {
-            let c = genre::ActiveModel {
-                name: ActiveValue::Set(name),
-                ..Default::default()
-            };
-            c.insert(&self.db).await.unwrap()
-        };
-        let intermediate = metadata_to_genre::ActiveModel {
-            metadata_id: ActiveValue::Set(metadata_id.to_owned()),
-            genre_id: ActiveValue::Set(db_genre.id),
-        };
-        intermediate.insert(&self.db).await.ok();
-        Ok(())
+        create_partial_metadata(data, &self.0.db).await
     }
 
     pub async fn update_seen_item(
@@ -1957,7 +1278,11 @@ ORDER BY RANDOM() LIMIT 10;
         user_id: String,
         input: UpdateSeenItemInput,
     ) -> Result<bool> {
-        let seen = match Seen::find_by_id(input.seen_id).one(&self.db).await.unwrap() {
+        let seen = match Seen::find_by_id(input.seen_id)
+            .one(&self.0.db)
+            .await
+            .unwrap()
+        {
             Some(s) => s,
             None => return Err(Error::new("No seen found for this user and metadata")),
         };
@@ -1980,7 +1305,10 @@ ORDER BY RANDOM() LIMIT 10;
         if let Some(review_id) = input.review_id {
             let (review, to_update_review_id) = match review_id.is_empty() {
                 false => (
-                    Review::find_by_id(&review_id).one(&self.db).await.unwrap(),
+                    Review::find_by_id(&review_id)
+                        .one(&self.0.db)
+                        .await
+                        .unwrap(),
                     Some(review_id),
                 ),
                 true => (None, None),
@@ -1994,147 +1322,19 @@ ORDER BY RANDOM() LIMIT 10;
             }
             seen.review_id = ActiveValue::Set(to_update_review_id);
         }
-        let seen = seen.update(&self.db).await.unwrap();
-        self.after_media_seen_tasks(seen).await?;
-        Ok(true)
-    }
-
-    pub async fn commit_metadata_internal(
-        &self,
-        details: MediaDetails,
-        is_partial: Option<bool>,
-    ) -> 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),
-            description: ActiveValue::Set(details.description),
-            publish_year: ActiveValue::Set(details.publish_year),
-            publish_date: ActiveValue::Set(details.publish_date),
-            images: ActiveValue::Set(Some(images)),
-            videos: ActiveValue::Set(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),
-            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(&self.db).await?;
-
-        self.change_metadata_associations(
-            &metadata.id,
-            metadata.lot,
-            metadata.source,
-            details.genres.clone(),
-            details.suggestions.clone(),
-            details.group_identifiers.clone(),
-            details.people.clone(),
-        )
-        .await?;
-        Ok(metadata)
-    }
-
-    #[allow(clippy::too_many_arguments)]
-    async fn change_metadata_associations(
-        &self,
-        metadata_id: &String,
-        lot: MediaLot,
-        source: MediaSource,
-        genres: Vec<String>,
-        suggestions: Vec<PartialMetadataWithoutId>,
-        groups: Vec<String>,
-        people: Vec<PartialMetadataPerson>,
-    ) -> Result<()> {
-        MetadataToPerson::delete_many()
-            .filter(metadata_to_person::Column::MetadataId.eq(metadata_id))
-            .exec(&self.db)
-            .await?;
-        MetadataToGenre::delete_many()
-            .filter(metadata_to_genre::Column::MetadataId.eq(metadata_id))
-            .exec(&self.db)
-            .await?;
-        MetadataToMetadata::delete_many()
-            .filter(metadata_to_metadata::Column::FromMetadataId.eq(metadata_id))
-            .filter(
-                metadata_to_metadata::Column::Relation.eq(MetadataToMetadataRelation::Suggestion),
-            )
-            .exec(&self.db)
-            .await?;
-        for (index, creator) in people.into_iter().enumerate() {
-            self.associate_person_with_metadata(metadata_id, creator, index)
-                .await
-                .ok();
-        }
-        for genre in genres {
-            self.associate_genre_with_metadata(genre, metadata_id)
-                .await
-                .ok();
-        }
-        for suggestion in suggestions {
-            self.associate_suggestion_with_metadata(suggestion, metadata_id)
-                .await
-                .ok();
-        }
-        for group_identifier in groups {
-            self.deploy_associate_group_with_metadata_job(lot, source, group_identifier)
-                .await
-                .ok();
-        }
-        Ok(())
-    }
-
-    pub async fn deploy_update_metadata_job(
-        &self,
-        metadata_id: &String,
-        force_update: bool,
-    ) -> Result<bool> {
-        self.perform_application_job
-            .clone()
-            .enqueue(ApplicationJob::UpdateMetadata(
-                metadata_id.to_owned(),
-                force_update,
-            ))
-            .await
-            .unwrap();
+        let seen = seen.update(&self.0.db).await.unwrap();
+        after_media_seen_tasks(seen, &self.0).await?;
         Ok(true)
     }
 
     pub async fn deploy_update_person_job(&self, person_id: String) -> Result<bool> {
         let person = Person::find_by_id(person_id)
-            .one(&self.db)
+            .one(&self.0.db)
             .await
             .unwrap()
             .unwrap();
-        self.perform_application_job
+        self.0
+            .perform_application_job
             .clone()
             .enqueue(ApplicationJob::UpdatePerson(person.id))
             .await
@@ -2147,11 +1347,12 @@ ORDER BY RANDOM() LIMIT 10;
         metadata_group_id: String,
     ) -> Result<bool> {
         let metadata_group = MetadataGroup::find_by_id(metadata_group_id)
-            .one(&self.db)
+            .one(&self.0.db)
             .await
             .unwrap()
             .unwrap();
-        self.perform_application_job
+        self.0
+            .perform_application_job
             .clone()
             .enqueue(ApplicationJob::UpdateMetadataGroup(metadata_group.id))
             .await
@@ -2165,7 +1366,7 @@ ORDER BY RANDOM() LIMIT 10;
         merge_from: String,
         merge_into: String,
     ) -> Result<bool> {
-        let txn = self.db.begin().await?;
+        let txn = self.0.db.begin().await?;
         for old_seen in Seen::find()
             .filter(seen::Column::MetadataId.eq(&merge_from))
             .filter(seen::Column::UserId.eq(&user_id))
@@ -2258,6 +1459,14 @@ ORDER BY RANDOM() LIMIT 10;
         Ok(true)
     }
 
+    pub async fn commit_metadata(&self, input: CommitMediaInput) -> Result<metadata::Model> {
+        commit_metadata(input, &self.0).await
+    }
+
+    pub async fn commit_person(&self, input: CommitPersonInput) -> Result<StringIdObject> {
+        commit_person(input, &self.0.db).await
+    }
+
     pub async fn disassociate_metadata(
         &self,
         user_id: String,
@@ -2266,17 +1475,17 @@ ORDER BY RANDOM() LIMIT 10;
         let delete_review = Review::delete_many()
             .filter(review::Column::MetadataId.eq(&metadata_id))
             .filter(review::Column::UserId.eq(&user_id))
-            .exec(&self.db)
+            .exec(&self.0.db)
             .await?;
         ryot_log!(debug, "Deleted {} reviews", delete_review.rows_affected);
         let delete_seen = Seen::delete_many()
             .filter(seen::Column::MetadataId.eq(&metadata_id))
             .filter(seen::Column::UserId.eq(&user_id))
-            .exec(&self.db)
+            .exec(&self.0.db)
             .await?;
         ryot_log!(debug, "Deleted {} seen items", delete_seen.rows_affected);
         let collections_part_of = entity_in_collections_with_collection_to_entity_ids(
-            &self.db,
+            &self.0.db,
             &user_id,
             &metadata_id,
             EntityLot::Metadata,
@@ -2286,7 +1495,7 @@ ORDER BY RANDOM() LIMIT 10;
         .map(|(_, id)| id);
         let delete_collections = CollectionToEntity::delete_many()
             .filter(collection_to_entity::Column::Id.is_in(collections_part_of))
-            .exec(&self.db)
+            .exec(&self.0.db)
             .await?;
         ryot_log!(
             debug,
@@ -2296,7 +1505,7 @@ ORDER BY RANDOM() LIMIT 10;
         UserToEntity::delete_many()
             .filter(user_to_entity::Column::MetadataId.eq(metadata_id))
             .filter(user_to_entity::Column::UserId.eq(user_id))
-            .exec(&self.db)
+            .exec(&self.0.db)
             .await?;
         Ok(true)
     }
@@ -2317,8 +1526,8 @@ ORDER BY RANDOM() LIMIT 10;
             });
         }
         let cloned_user_id = user_id.to_owned();
-        let preferences = user_preferences_by_id(&self.db, user_id, &self.config).await?;
-        let provider = self.get_metadata_provider(input.lot, input.source).await?;
+        let preferences = user_preferences_by_id(user_id, &self.0).await?;
+        let provider = get_metadata_provider(input.lot, input.source, &self.0).await?;
         let results = provider
             .metadata_search(&query, input.search.page, preferences.general.display_nsfw)
             .await?;
@@ -2353,7 +1562,7 @@ ORDER BY RANDOM() LIMIT 10;
             .filter(metadata::Column::Source.eq(input.source))
             .filter(metadata::Column::Identifier.is_in(&all_identifiers))
             .into_tuple::<(String, String, bool)>()
-            .all(&self.db)
+            .all(&self.0.db)
             .await?
             .into_iter()
             .map(|(key, value1, value2)| (key, (value1, value2)));
@@ -2392,7 +1601,7 @@ ORDER BY RANDOM() LIMIT 10;
                 items: vec![],
             });
         }
-        let preferences = user_preferences_by_id(&self.db, user_id, &self.config).await?;
+        let preferences = user_preferences_by_id(user_id, &self.0).await?;
         let provider = self.get_non_metadata_provider(input.source).await?;
         let results = provider
             .people_search(
@@ -2420,169 +1629,66 @@ ORDER BY RANDOM() LIMIT 10;
                 items: vec![],
             });
         }
-        let preferences = user_preferences_by_id(&self.db, user_id, &self.config).await?;
-        let provider = self.get_metadata_provider(input.lot, input.source).await?;
+        let preferences = user_preferences_by_id(user_id, &self.0).await?;
+        let provider = get_metadata_provider(input.lot, input.source, &self.0).await?;
         let results = provider
             .metadata_group_search(&query, input.search.page, preferences.general.display_nsfw)
             .await?;
         Ok(results)
     }
 
-    pub async fn get_openlibrary_service(&self) -> Result<OpenlibraryService> {
-        Ok(OpenlibraryService::new(
-            &self.config.books.openlibrary,
-            self.config.frontend.page_size,
-        )
-        .await)
-    }
-
-    pub async fn get_isbn_service(&self) -> Result<GoogleBooksService> {
-        Ok(GoogleBooksService::new(
-            &self.config.books.google_books,
-            self.config.frontend.page_size,
-        )
-        .await)
-    }
-
-    async fn get_metadata_provider(&self, lot: MediaLot, source: MediaSource) -> Result<Provider> {
+    async fn get_non_metadata_provider(&self, source: MediaSource) -> Result<Provider> {
         let err = || Err(Error::new("This source is not supported".to_owned()));
         let service: Provider = match source {
             MediaSource::Vndb => Box::new(
-                VndbService::new(&self.config.visual_novels, self.config.frontend.page_size).await,
-            ),
-            MediaSource::Openlibrary => Box::new(self.get_openlibrary_service().await?),
-            MediaSource::Itunes => Box::new(
-                ITunesService::new(&self.config.podcasts.itunes, self.config.frontend.page_size)
-                    .await,
-            ),
-            MediaSource::GoogleBooks => Box::new(self.get_isbn_service().await?),
-            MediaSource::Audible => Box::new(
-                AudibleService::new(
-                    &self.config.audio_books.audible,
-                    self.config.frontend.page_size,
+                VndbService::new(
+                    &self.0.config.visual_novels,
+                    self.0.config.frontend.page_size,
                 )
                 .await,
             ),
-            MediaSource::Listennotes => Box::new(
-                ListennotesService::new(&self.config.podcasts, self.config.frontend.page_size)
-                    .await,
-            ),
-            MediaSource::Tmdb => match lot {
-                MediaLot::Show => Box::new(
-                    TmdbShowService::new(
-                        &self.config.movies_and_shows.tmdb,
-                        *self.timezone,
-                        self.config.frontend.page_size,
-                    )
-                    .await,
-                ),
-                MediaLot::Movie => Box::new(
-                    TmdbMovieService::new(
-                        &self.config.movies_and_shows.tmdb,
-                        *self.timezone,
-                        self.config.frontend.page_size,
-                    )
-                    .await,
-                ),
-                _ => return err(),
-            },
-            MediaSource::Anilist => match lot {
-                MediaLot::Anime => Box::new(
-                    AnilistAnimeService::new(
-                        &self.config.anime_and_manga.anilist,
-                        self.config.frontend.page_size,
-                    )
-                    .await,
-                ),
-                MediaLot::Manga => Box::new(
-                    AnilistMangaService::new(
-                        &self.config.anime_and_manga.anilist,
-                        self.config.frontend.page_size,
-                    )
-                    .await,
-                ),
-                _ => return err(),
-            },
-            MediaSource::Mal => match lot {
-                MediaLot::Anime => Box::new(
-                    MalAnimeService::new(
-                        &self.config.anime_and_manga.mal,
-                        self.config.frontend.page_size,
-                    )
-                    .await,
-                ),
-                MediaLot::Manga => Box::new(
-                    MalMangaService::new(
-                        &self.config.anime_and_manga.mal,
-                        self.config.frontend.page_size,
-                    )
-                    .await,
-                ),
-                _ => return err(),
-            },
-            MediaSource::Igdb => Box::new(
-                IgdbService::new(&self.config.video_games, self.config.frontend.page_size).await,
-            ),
-            MediaSource::MangaUpdates => Box::new(
-                MangaUpdatesService::new(
-                    &self.config.anime_and_manga.manga_updates,
-                    self.config.frontend.page_size,
+            MediaSource::Openlibrary => Box::new(get_openlibrary_service(&self.0.config).await?),
+            MediaSource::Itunes => Box::new(
+                ITunesService::new(
+                    &self.0.config.podcasts.itunes,
+                    self.0.config.frontend.page_size,
                 )
                 .await,
             ),
-            MediaSource::Custom => return err(),
-        };
-        Ok(service)
-    }
-
-    pub async fn get_tmdb_non_media_service(&self) -> Result<NonMediaTmdbService> {
-        Ok(NonMediaTmdbService::new(&self.config.movies_and_shows.tmdb, *self.timezone).await)
-    }
-
-    async fn get_non_metadata_provider(&self, source: MediaSource) -> Result<Provider> {
-        let err = || Err(Error::new("This source is not supported".to_owned()));
-        let service: Provider = match source {
-            MediaSource::Vndb => Box::new(
-                VndbService::new(&self.config.visual_novels, self.config.frontend.page_size).await,
-            ),
-            MediaSource::Openlibrary => Box::new(self.get_openlibrary_service().await?),
-            MediaSource::Itunes => Box::new(
-                ITunesService::new(&self.config.podcasts.itunes, self.config.frontend.page_size)
-                    .await,
-            ),
             MediaSource::GoogleBooks => Box::new(
                 GoogleBooksService::new(
-                    &self.config.books.google_books,
-                    self.config.frontend.page_size,
+                    &self.0.config.books.google_books,
+                    self.0.config.frontend.page_size,
                 )
                 .await,
             ),
             MediaSource::Audible => Box::new(
                 AudibleService::new(
-                    &self.config.audio_books.audible,
-                    self.config.frontend.page_size,
+                    &self.0.config.audio_books.audible,
+                    self.0.config.frontend.page_size,
                 )
                 .await,
             ),
             MediaSource::Listennotes => Box::new(
-                ListennotesService::new(&self.config.podcasts, self.config.frontend.page_size)
+                ListennotesService::new(&self.0.config.podcasts, self.0.config.frontend.page_size)
                     .await,
             ),
             MediaSource::Igdb => Box::new(
-                IgdbService::new(&self.config.video_games, self.config.frontend.page_size).await,
+                IgdbService::new(&self.0.config.video_games, self.0.config.frontend.page_size)
+                    .await,
             ),
             MediaSource::MangaUpdates => Box::new(
                 MangaUpdatesService::new(
-                    &self.config.anime_and_manga.manga_updates,
-                    self.config.frontend.page_size,
+                    &self.0.config.anime_and_manga.manga_updates,
+                    self.0.config.frontend.page_size,
                 )
                 .await,
             ),
-            MediaSource::Tmdb => Box::new(self.get_tmdb_non_media_service().await?),
+            MediaSource::Tmdb => Box::new(get_tmdb_non_media_service(&self.0).await?),
             MediaSource::Anilist => Box::new(
                 NonMediaAnilistService::new(
-                    &self.config.anime_and_manga.anilist,
-                    self.config.frontend.page_size,
+                    &self.0.config.anime_and_manga.anilist,
+                    self.0.config.frontend.page_size,
                 )
                 .await,
             ),
@@ -2592,84 +1698,16 @@ ORDER BY RANDOM() LIMIT 10;
         Ok(service)
     }
 
-    async fn details_from_provider(
-        &self,
-        lot: MediaLot,
-        source: MediaSource,
-        identifier: &str,
-    ) -> Result<MediaDetails> {
-        let provider = self.get_metadata_provider(lot, source).await?;
-        let results = provider.metadata_details(identifier).await?;
-        Ok(results)
-    }
-
-    pub async fn commit_metadata(&self, input: CommitMediaInput) -> Result<metadata::Model> {
-        if 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()))
-            .one(&self.db)
-            .await?
-        {
-            let cached_metadata = CommitCache {
-                id: m.id.clone(),
-                lot: EntityLot::Metadata,
-            };
-            if self.commit_cache.get(&cached_metadata).await.is_some() {
-                return Ok(m);
-            }
-            self.commit_cache.insert(cached_metadata.clone(), ()).await;
-            if input.force_update.unwrap_or_default() {
-                ryot_log!(debug, "Forcing update of metadata with id {}", &m.id);
-                self.update_metadata_and_notify_users(&m.id, true).await?;
-            }
-            self.commit_cache.remove(&cached_metadata).await;
-            Ok(m)
-        } else {
-            let details = self
-                .details_from_provider(input.lot, input.source, &input.identifier)
-                .await?;
-            let media = self.commit_metadata_internal(details, None).await?;
-            Ok(media)
-        }
-    }
-
-    pub async fn commit_person(&self, input: CommitPersonInput) -> 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(&self.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(&self.db).await?;
-            Ok(StringIdObject { id: person.id })
-        }
-    }
-
     pub async fn commit_metadata_group(&self, input: CommitMediaInput) -> Result<StringIdObject> {
-        let (group_id, associated_items) = self
-            .commit_metadata_group_internal(&input.identifier, input.lot, input.source)
-            .await?;
+        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 = self.create_partial_metadata(media).await?;
+            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.db)
+                .exec(&self.0.db)
                 .await
                 .ok();
             let intermediate = metadata_to_metadata_group::ActiveModel {
@@ -2677,7 +1715,7 @@ ORDER BY RANDOM() LIMIT 10;
                 metadata_id: ActiveValue::Set(db_partial_metadata.id),
                 part: ActiveValue::Set((idx + 1).try_into().unwrap()),
             };
-            intermediate.insert(&self.db).await.ok();
+            intermediate.insert(&self.0.db).await.ok();
         }
         Ok(StringIdObject { id: group_id })
     }
@@ -2687,146 +1725,26 @@ ORDER BY RANDOM() LIMIT 10;
         user_id: &String,
         input: CreateOrUpdateReviewInput,
     ) -> Result<StringIdObject> {
-        let preferences = user_preferences_by_id(&self.db, user_id, &self.config).await?;
-        if preferences.general.disable_reviews {
-            return Err(Error::new("Reviews are disabled"));
-        }
-        let show_ei = if let (Some(season), Some(episode)) =
-            (input.show_season_number, input.show_episode_number)
-        {
-            Some(SeenShowExtraInformation { season, episode })
-        } else {
-            None
-        };
-        let podcast_ei = input
-            .podcast_episode_number
-            .map(|episode| SeenPodcastExtraInformation { episode });
-        let anime_ei = input
-            .anime_episode_number
-            .map(|episode| SeenAnimeExtraInformation {
-                episode: Some(episode),
-            });
-        let manga_ei =
-            if input.manga_chapter_number.is_none() && input.manga_volume_number.is_none() {
-                None
-            } else {
-                Some(SeenMangaExtraInformation {
-                    chapter: input.manga_chapter_number,
-                    volume: input.manga_volume_number,
-                })
-            };
-
-        if input.rating.is_none() && input.text.is_none() {
-            return Err(Error::new("At-least one of rating or review is required."));
-        }
-        let mut review_obj = review::ActiveModel {
-            id: match input.review_id.clone() {
-                Some(i) => ActiveValue::Unchanged(i),
-                None => ActiveValue::NotSet,
-            },
-            rating: ActiveValue::Set(input.rating.map(
-                |r| match preferences.general.review_scale {
-                    UserReviewScale::OutOfFive => r * dec!(20),
-                    UserReviewScale::OutOfHundred | UserReviewScale::ThreePointSmiley => r,
-                },
-            )),
-            text: ActiveValue::Set(input.text),
-            user_id: ActiveValue::Set(user_id.to_owned()),
-            show_extra_information: ActiveValue::Set(show_ei),
-            anime_extra_information: ActiveValue::Set(anime_ei),
-            manga_extra_information: ActiveValue::Set(manga_ei),
-            podcast_extra_information: ActiveValue::Set(podcast_ei),
-            comments: ActiveValue::Set(vec![]),
-            ..Default::default()
-        };
-        let entity_id = input.entity_id.clone();
-        match input.entity_lot {
-            EntityLot::Metadata => review_obj.metadata_id = ActiveValue::Set(Some(entity_id)),
-            EntityLot::Person => review_obj.person_id = ActiveValue::Set(Some(entity_id)),
-            EntityLot::MetadataGroup => {
-                review_obj.metadata_group_id = ActiveValue::Set(Some(entity_id))
-            }
-            EntityLot::Collection => review_obj.collection_id = ActiveValue::Set(Some(entity_id)),
-            EntityLot::Exercise => review_obj.exercise_id = ActiveValue::Set(Some(entity_id)),
-            EntityLot::Workout | EntityLot::WorkoutTemplate => unreachable!(),
-        };
-        if let Some(s) = input.is_spoiler {
-            review_obj.is_spoiler = ActiveValue::Set(s);
-        }
-        if let Some(v) = input.visibility {
-            review_obj.visibility = ActiveValue::Set(v);
-        }
-        if let Some(d) = input.date {
-            review_obj.posted_on = ActiveValue::Set(d);
-        }
-        let insert = review_obj.save(&self.db).await.unwrap();
-        if insert.visibility.unwrap() == Visibility::Public {
-            let entity_lot = insert.entity_lot.unwrap();
-            let id = insert.entity_id.unwrap();
-            let obj_title = match entity_lot {
-                EntityLot::Metadata => {
-                    Metadata::find_by_id(&id)
-                        .one(&self.db)
-                        .await?
-                        .unwrap()
-                        .title
-                }
-                EntityLot::MetadataGroup => {
-                    MetadataGroup::find_by_id(&id)
-                        .one(&self.db)
-                        .await?
-                        .unwrap()
-                        .title
-                }
-                EntityLot::Person => Person::find_by_id(&id).one(&self.db).await?.unwrap().name,
-                EntityLot::Collection => {
-                    Collection::find_by_id(&id)
-                        .one(&self.db)
-                        .await?
-                        .unwrap()
-                        .name
-                }
-                EntityLot::Exercise => id.clone(),
-                EntityLot::Workout | EntityLot::WorkoutTemplate => unreachable!(),
-            };
-            let user = user_by_id(&self.db, &insert.user_id.unwrap()).await?;
-            // DEV: Do not send notification if updating a review
-            if input.review_id.is_none() {
-                self.perform_core_application_job
-                    .clone()
-                    .enqueue(CoreApplicationJob::ReviewPosted(ReviewPostedEvent {
-                        obj_title,
-                        entity_lot,
-                        obj_id: id,
-                        username: user.name,
-                        review_id: insert.id.clone().unwrap(),
-                    }))
-                    .await
-                    .unwrap();
-            }
-        }
-        Ok(StringIdObject {
-            id: insert.id.unwrap(),
-        })
+        post_review(user_id, input, &self.0).await
     }
 
     pub async fn delete_review(&self, user_id: String, review_id: String) -> Result<bool> {
         let review = Review::find()
             .filter(review::Column::Id.eq(review_id))
-            .one(&self.db)
+            .one(&self.0.db)
             .await
             .unwrap();
         match review {
             Some(r) => {
                 if r.user_id == user_id {
                     associate_user_with_entity(
-                        &self.db,
+                        &self.0.db,
                         &user_id,
                         r.entity_id.clone(),
                         r.entity_lot,
                     )
                     .await?;
-                    r.delete(&self.db).await?;
+                    r.delete(&self.0.db).await?;
                     Ok(true)
                 } else {
                     Err(Error::new("This review does not belong to you".to_owned()))
@@ -2841,7 +1759,7 @@ ORDER BY RANDOM() LIMIT 10;
         user_id: &String,
         seen_id: String,
     ) -> Result<StringIdObject> {
-        let seen_item = Seen::find_by_id(seen_id).one(&self.db).await.unwrap();
+        let seen_item = Seen::find_by_id(seen_id).one(&self.0.db).await.unwrap();
         if let Some(si) = seen_item {
             let cloned_seen = si.clone();
             let (ssn, sen) = match &si.show_extra_information {
@@ -2862,7 +1780,7 @@ ORDER BY RANDOM() LIMIT 10;
                 user_id: user_id.to_owned(),
                 metadata_id: si.metadata_id.clone(),
             };
-            self.cache_service.delete(cache).await?;
+            self.0.cache_service.delete(cache).await?;
             let seen_id = si.id.clone();
             let metadata_id = si.metadata_id.clone();
             if &si.user_id != user_id {
@@ -2870,101 +1788,26 @@ ORDER BY RANDOM() LIMIT 10;
                     "This seen item does not belong to this user".to_owned(),
                 ));
             }
-            si.delete(&self.db).await.trace_ok();
-            associate_user_with_entity(&self.db, user_id, metadata_id, EntityLot::Metadata).await?;
-            self.after_media_seen_tasks(cloned_seen).await?;
+            si.delete(&self.0.db).await.trace_ok();
+            associate_user_with_entity(&self.0.db, user_id, metadata_id, EntityLot::Metadata)
+                .await?;
+            after_media_seen_tasks(cloned_seen, &self.0).await?;
             Ok(StringIdObject { id: seen_id })
         } else {
             Err(Error::new("This seen item does not exist".to_owned()))
         }
     }
 
-    async fn update_metadata(
-        &self,
-        metadata_id: &String,
-        force_update: bool,
-    ) -> Result<Vec<(String, MediaStateChanged)>> {
-        let metadata = Metadata::find_by_id(metadata_id)
-            .one(&self.db)
-            .await
-            .unwrap()
-            .unwrap();
-        if !force_update {
-            // check whether the metadata needs to be updated
-            let provider = self
-                .get_metadata_provider(metadata.lot, metadata.source)
-                .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))
-            .col_expr(metadata::Column::IsPartial, Expr::value(false))
-            .exec(&self.db)
-            .await?;
-        let maybe_details = self
-            .details_from_provider(metadata.lot, metadata.source, &metadata.identifier)
-            .await;
-        let notifications = match maybe_details {
-            Ok(details) => self.update_media(metadata_id, details).await?,
-            Err(e) => {
-                ryot_log!(
-                    error,
-                    "Error while updating metadata = {:?}: {:?}",
-                    metadata_id,
-                    e
-                );
-                vec![]
-            }
-        };
-        ryot_log!(debug, "Updated metadata for {:?}", metadata_id);
-        Ok(notifications)
-    }
-
-    pub async fn update_metadata_and_notify_users(
-        &self,
-        metadata_id: &String,
-        force_update: bool,
-    ) -> Result<()> {
-        let notifications = self
-            .update_metadata(metadata_id, force_update)
-            .await
-            .unwrap();
-        if !notifications.is_empty() {
-            let users_to_notify = self
-                .get_entities_monitored_by(metadata_id, EntityLot::Metadata)
-                .await?;
-            for notification in notifications {
-                for user_id in users_to_notify.iter() {
-                    self.queue_media_state_changed_notification_for_user(user_id, &notification)
-                        .await
-                        .trace_ok();
-                }
-            }
-        }
-        Ok(())
-    }
-
     async fn regenerate_user_summaries(&self) -> Result<()> {
         let all_users = User::find()
             .select_only()
             .column(user::Column::Id)
             .into_tuple::<String>()
-            .all(&self.db)
+            .all(&self.0.db)
             .await
             .unwrap();
         for user_id in all_users {
-            calculate_user_activities_and_summary(&self.db, &user_id, false).await?;
+            calculate_user_activities_and_summary(&self.0.db, &user_id, false).await?;
         }
         Ok(())
     }
@@ -3033,11 +1876,8 @@ ORDER BY RANDOM() LIMIT 10;
             visual_novel_specifics: input.visual_novel_specifics,
             ..Default::default()
         };
-        let media = self
-            .commit_metadata_internal(details, Some(is_partial))
-            .await?;
+        let media = commit_metadata_internal(details, Some(is_partial), &self.0).await?;
         add_entity_to_collection(
-            &self.db,
             &user_id,
             ChangeCollectionToEntityInput {
                 creator_user_id: user_id.to_owned(),
@@ -3046,7 +1886,7 @@ ORDER BY RANDOM() LIMIT 10;
                 entity_lot: EntityLot::Metadata,
                 ..Default::default()
             },
-            &self.perform_core_application_job,
+            &self.0,
         )
         .await?;
         Ok(media)
@@ -3119,485 +1959,29 @@ ORDER BY RANDOM() LIMIT 10;
             .collect()
     }
 
-    pub async fn yank_integrations_data_for_user(&self, user_id: &String) -> Result<bool> {
-        let preferences = user_preferences_by_id(&self.db, user_id, &self.config).await?;
-        if preferences.general.disable_integrations {
-            return Ok(false);
-        }
-        let integrations = Integration::find()
-            .filter(integration::Column::UserId.eq(user_id))
-            .filter(integration::Column::Lot.eq(IntegrationLot::Yank))
-            .all(&self.db)
-            .await?;
-        let mut progress_updates = vec![];
-        let mut collection_updates = vec![];
-        let mut to_update_integrations = vec![];
-        let integration_service = self.get_integration_service();
-        for integration in integrations.into_iter() {
-            if integration.is_disabled.unwrap_or_default() {
-                ryot_log!(debug, "Integration {} is disabled", integration.id);
-                continue;
-            }
-            let specifics = integration.clone().provider_specifics.unwrap();
-            let integration_input = match integration.provider {
-                IntegrationProvider::Audiobookshelf => IntegrationType::Audiobookshelf(
-                    specifics.audiobookshelf_base_url.unwrap(),
-                    specifics.audiobookshelf_token.unwrap(),
-                    integration.sync_to_owned_collection,
-                    self.get_isbn_service().await.unwrap(),
-                ),
-                IntegrationProvider::Komga => IntegrationType::Komga(
-                    specifics.komga_base_url.unwrap(),
-                    specifics.komga_username.unwrap(),
-                    specifics.komga_password.unwrap(),
-                    specifics.komga_provider.unwrap(),
-                    integration.sync_to_owned_collection,
-                ),
-                _ => continue,
-            };
-            let response = integration_service
-                .process_progress_commit(integration_input, |input| self.commit_metadata(input))
-                .await;
-            if let Ok((seen_progress, collection_progress)) = response {
-                collection_updates.extend(collection_progress);
-                to_update_integrations.push(integration.id.clone());
-                progress_updates.push((integration, seen_progress));
-            }
-        }
-        for (integration, progress_updates) in progress_updates.into_iter() {
-            for pu in progress_updates.into_iter() {
-                self.integration_progress_update(&integration, pu, user_id)
-                    .await
-                    .trace_ok();
-            }
-        }
-        for col_update in collection_updates.into_iter() {
-            let metadata_result = self
-                .commit_metadata(CommitMediaInput {
-                    lot: col_update.lot,
-                    source: col_update.source,
-                    identifier: col_update.identifier.clone(),
-                    force_update: None,
-                })
-                .await;
-
-            if let Ok(metadata::Model { id, .. }) = metadata_result {
-                add_entity_to_collection(
-                    &self.db,
-                    user_id,
-                    ChangeCollectionToEntityInput {
-                        creator_user_id: user_id.to_owned(),
-                        collection_name: col_update.collection,
-                        entity_id: id.clone(),
-                        entity_lot: EntityLot::Metadata,
-                        ..Default::default()
-                    },
-                    &self.perform_core_application_job,
-                )
-                .await
-                .trace_ok();
-            }
-        }
-        Integration::update_many()
-            .filter(integration::Column::Id.is_in(to_update_integrations))
-            .col_expr(
-                integration::Column::LastTriggeredOn,
-                Expr::value(Utc::now()),
-            )
-            .exec(&self.db)
-            .await?;
-        Ok(true)
-    }
-
-    async fn yank_integrations_data(&self) -> Result<()> {
-        let users_with_integrations = Integration::find()
-            .filter(integration::Column::Lot.eq(IntegrationLot::Yank))
-            .select_only()
-            .column(integration::Column::UserId)
-            .into_tuple::<String>()
-            .all(&self.db)
-            .await?;
-        for user_id in users_with_integrations {
-            ryot_log!(debug, "Yanking integrations data for user {}", user_id);
-            self.yank_integrations_data_for_user(&user_id).await?;
-        }
-        Ok(())
-    }
-
-    pub async fn sync_integrations_data(&self) -> Result<()> {
-        ryot_log!(trace, "Syncing integrations data...");
-        self.yank_integrations_data().await.unwrap();
-        Ok(())
-    }
-
     pub async fn handle_entity_added_to_collection_event(
         &self,
         user_id: String,
         collection_to_entity_id: Uuid,
     ) -> Result<()> {
-        let cte = CollectionToEntity::find_by_id(collection_to_entity_id)
-            .one(&self.db)
-            .await?
-            .ok_or_else(|| Error::new("Collection to entity does not exist"))?;
-        if !matches!(cte.entity_lot, EntityLot::Metadata) {
-            return Ok(());
-        }
-        let integration_service = self.get_integration_service();
-        let integrations = Integration::find()
-            .filter(integration::Column::UserId.eq(user_id))
-            .filter(integration::Column::Lot.eq(IntegrationLot::Push))
-            .all(&self.db)
-            .await?;
-        for integration in integrations {
-            let possible_collection_ids = match integration.provider_specifics.clone() {
-                Some(s) => match integration.provider {
-                    IntegrationProvider::Radarr => s.radarr_sync_collection_ids.unwrap_or_default(),
-                    IntegrationProvider::Sonarr => s.sonarr_sync_collection_ids.unwrap_or_default(),
-                    _ => vec![],
-                },
-                None => vec![],
-            };
-            if !possible_collection_ids.contains(&cte.collection_id) {
-                continue;
-            }
-            let specifics = integration.provider_specifics.unwrap();
-            let metadata = Metadata::find_by_id(&cte.entity_id)
-                .one(&self.db)
-                .await?
-                .ok_or_else(|| Error::new("Metadata does not exist"))?;
-            let maybe_entity_id = match metadata.lot {
-                MediaLot::Show => metadata
-                    .external_identifiers
-                    .and_then(|ei| ei.tvdb_id.map(|i| i.to_string())),
-                _ => Some(metadata.identifier.clone()),
-            };
-            if let Some(entity_id) = maybe_entity_id {
-                let _push_result = match integration.provider {
-                    IntegrationProvider::Radarr => {
-                        integration_service
-                            .push(IntegrationType::Radarr(
-                                specifics.radarr_base_url.unwrap(),
-                                specifics.radarr_api_key.unwrap(),
-                                specifics.radarr_profile_id.unwrap(),
-                                specifics.radarr_root_folder_path.unwrap(),
-                                entity_id,
-                            ))
-                            .await
-                    }
-                    IntegrationProvider::Sonarr => {
-                        integration_service
-                            .push(IntegrationType::Sonarr(
-                                specifics.sonarr_base_url.unwrap(),
-                                specifics.sonarr_api_key.unwrap(),
-                                specifics.sonarr_profile_id.unwrap(),
-                                specifics.sonarr_root_folder_path.unwrap(),
-                                entity_id,
-                            ))
-                            .await
-                    }
-                    _ => unreachable!(),
-                };
-            }
-        }
-        Ok(())
-    }
-
-    pub async fn process_integration_webhook(
-        &self,
-        integration_slug: String,
-        payload: String,
-    ) -> Result<String> {
-        ryot_log!(
-            debug,
-            "Processing integration webhook for slug: {}",
-            integration_slug
-        );
-        let integration = Integration::find_by_id(integration_slug)
-            .one(&self.db)
-            .await?
-            .ok_or_else(|| Error::new("Integration does not exist".to_owned()))?;
-        let preferences =
-            user_preferences_by_id(&self.db, &integration.user_id, &self.config).await?;
-        if integration.is_disabled.unwrap_or_default() || preferences.general.disable_integrations {
-            return Err(Error::new("Integration is disabled".to_owned()));
-        }
-        let service = self.get_integration_service();
-        let maybe_progress_update = match integration.provider {
-            IntegrationProvider::Kodi => {
-                service
-                    .process_progress(IntegrationType::Kodi(payload.clone()))
-                    .await
-            }
-            IntegrationProvider::Emby => {
-                service
-                    .process_progress(IntegrationType::Emby(payload.clone()))
-                    .await
-            }
-            IntegrationProvider::Jellyfin => {
-                service
-                    .process_progress(IntegrationType::Jellyfin(payload.clone()))
-                    .await
-            }
-            IntegrationProvider::Plex => {
-                let specifics = integration.clone().provider_specifics.unwrap();
-                service
-                    .process_progress(IntegrationType::Plex(
-                        payload.clone(),
-                        specifics.plex_username,
-                    ))
-                    .await
-            }
-            _ => return Err(Error::new("Unsupported integration source".to_owned())),
-        };
-        match maybe_progress_update {
-            Ok(pu) => {
-                let media_vec = pu.0;
-                for media in media_vec {
-                    self.integration_progress_update(
-                        &integration,
-                        media.clone(),
-                        &integration.user_id,
-                    )
-                    .await?;
-                }
-                let mut to_update: integration::ActiveModel = integration.into();
-                to_update.last_triggered_on = ActiveValue::Set(Some(Utc::now()));
-                to_update.update(&self.db).await?;
-                Ok("Progress updated successfully".to_owned())
-            }
-            Err(e) => Err(Error::new(e.to_string())),
-        }
-    }
-
-    async fn integration_progress_update(
-        &self,
-        integration: &integration::Model,
-        pu: IntegrationMediaSeen,
-        user_id: &String,
-    ) -> Result<()> {
-        if pu.progress < integration.minimum_progress.unwrap() {
-            return Ok(());
-        }
-        let progress = if pu.progress > integration.maximum_progress.unwrap() {
-            dec!(100)
-        } else {
-            pu.progress
-        };
-        let metadata::Model { id, .. } = self
-            .commit_metadata(CommitMediaInput {
-                lot: pu.lot,
-                source: pu.source,
-                identifier: pu.identifier,
-                force_update: None,
-            })
-            .await?;
-        if let Err(err) = self
-            .progress_update(
-                ProgressUpdateInput {
-                    metadata_id: id,
-                    progress: Some(progress),
-                    date: Some(get_current_date(&self.timezone)),
-                    show_season_number: pu.show_season_number,
-                    show_episode_number: pu.show_episode_number,
-                    podcast_episode_number: pu.podcast_episode_number,
-                    anime_episode_number: pu.anime_episode_number,
-                    manga_chapter_number: pu.manga_chapter_number,
-                    manga_volume_number: pu.manga_volume_number,
-                    provider_watched_on: pu.provider_watched_on,
-                    change_state: None,
-                },
+        self.0
+            .perform_application_job
+            .enqueue(ApplicationJob::HandleEntityAddedToCollectionEvent(
                 user_id,
-                true,
-            )
-            .await
-        {
-            ryot_log!(debug, "Error updating progress: {:?}", err);
-        };
-        Ok(())
-    }
-
-    async fn after_media_seen_tasks(&self, seen: seen::Model) -> Result<()> {
-        let add_entity_to_collection = |collection_name: &str| {
-            add_entity_to_collection(
-                &self.db,
-                &seen.user_id,
-                ChangeCollectionToEntityInput {
-                    creator_user_id: seen.user_id.clone(),
-                    collection_name: collection_name.to_string(),
-                    entity_id: seen.metadata_id.clone(),
-                    entity_lot: EntityLot::Metadata,
-                    ..Default::default()
-                },
-                &self.perform_core_application_job,
-            )
-        };
-        let remove_entity_from_collection = |collection_name: &str| {
-            remove_entity_from_collection(
-                &self.db,
-                &seen.user_id,
-                ChangeCollectionToEntityInput {
-                    creator_user_id: seen.user_id.clone(),
-                    collection_name: collection_name.to_string(),
-                    entity_id: seen.metadata_id.clone(),
-                    entity_lot: EntityLot::Metadata,
-                    ..Default::default()
-                },
-            )
-        };
-        remove_entity_from_collection(&DefaultCollection::Watchlist.to_string())
+                collection_to_entity_id,
+            ))
             .await
-            .ok();
-        match seen.state {
-            SeenState::InProgress => {
-                for col in &[DefaultCollection::InProgress, DefaultCollection::Monitoring] {
-                    add_entity_to_collection(&col.to_string()).await.ok();
-                }
-            }
-            SeenState::Dropped | SeenState::OnAHold => {
-                remove_entity_from_collection(&DefaultCollection::InProgress.to_string())
-                    .await
-                    .ok();
-            }
-            SeenState::Completed => {
-                let metadata = self.generic_metadata(&seen.metadata_id).await?;
-                if metadata.model.lot == MediaLot::Podcast
-                    || metadata.model.lot == MediaLot::Show
-                    || metadata.model.lot == MediaLot::Anime
-                    || metadata.model.lot == MediaLot::Manga
-                {
-                    let (is_complete, _) = self
-                        .is_metadata_finished_by_user(&seen.user_id, &metadata)
-                        .await?;
-                    if is_complete {
-                        remove_entity_from_collection(&DefaultCollection::InProgress.to_string())
-                            .await
-                            .ok();
-                        add_entity_to_collection(&DefaultCollection::Completed.to_string())
-                            .await
-                            .ok();
-                    } else {
-                        for col in &[DefaultCollection::InProgress, DefaultCollection::Monitoring] {
-                            add_entity_to_collection(&col.to_string()).await.ok();
-                        }
-                    }
-                } else {
-                    add_entity_to_collection(&DefaultCollection::Completed.to_string())
-                        .await
-                        .ok();
-                    for col in &[DefaultCollection::InProgress, DefaultCollection::Monitoring] {
-                        remove_entity_from_collection(&col.to_string()).await.ok();
-                    }
-                };
-            }
-        };
+            .unwrap();
         Ok(())
     }
 
-    async fn is_metadata_finished_by_user(
-        &self,
-        user_id: &String,
-        metadata: &MetadataBaseData,
-    ) -> Result<(bool, Vec<seen::Model>)> {
-        let metadata = metadata.clone();
-        let seen_history = self.seen_history(user_id, &metadata.model.id).await?;
-        let is_finished = if metadata.model.lot == MediaLot::Podcast
-            || metadata.model.lot == MediaLot::Show
-            || metadata.model.lot == MediaLot::Anime
-            || metadata.model.lot == MediaLot::Manga
-        {
-            // DEV: If all episodes have been seen the same number of times, the media can be
-            // considered finished.
-            let all_episodes = if let Some(s) = metadata.model.show_specifics {
-                s.seasons
-                    .into_iter()
-                    .filter(|s| !SHOW_SPECIAL_SEASON_NAMES.contains(&s.name.as_str()))
-                    .flat_map(|s| {
-                        s.episodes
-                            .into_iter()
-                            .map(move |e| format!("{}-{}", s.season_number, e.episode_number))
-                    })
-                    .collect_vec()
-            } else if let Some(p) = metadata.model.podcast_specifics {
-                p.episodes
-                    .into_iter()
-                    .map(|e| format!("{}", e.number))
-                    .collect_vec()
-            } else if let Some(e) = metadata.model.anime_specifics.and_then(|a| a.episodes) {
-                (1..e + 1).map(|e| format!("{}", e)).collect_vec()
-            } else if let Some(c) = metadata.model.manga_specifics.and_then(|m| m.chapters) {
-                let one = Decimal::one();
-                (0..c.to_u32().unwrap_or(0))
-                    .map(|i| Decimal::from(i) + one)
-                    .map(|d| d.to_string())
-                    .collect_vec()
-            } else {
-                vec![]
-            };
-            if all_episodes.is_empty() {
-                return Ok((true, seen_history));
-            }
-            let mut bag =
-                HashMap::<String, i32>::from_iter(all_episodes.iter().cloned().map(|e| (e, 0)));
-            seen_history
-                .clone()
-                .into_iter()
-                .map(|h| {
-                    if let Some(s) = h.show_extra_information {
-                        format!("{}-{}", s.season, s.episode)
-                    } else if let Some(p) = h.podcast_extra_information {
-                        format!("{}", p.episode)
-                    } else if let Some(a) = h.anime_extra_information.and_then(|a| a.episode) {
-                        format!("{}", a)
-                    } else if let Some(m) = h.manga_extra_information.and_then(|m| m.chapter) {
-                        format!("{}", m)
-                    } else {
-                        String::new()
-                    }
-                })
-                .for_each(|ep| {
-                    bag.entry(ep).and_modify(|c| *c += 1);
-                });
-            let values = bag.values().cloned().collect_vec();
-
-            let min_value = values.iter().min();
-            let max_value = values.iter().max();
-
-            match (min_value, max_value) {
-                (Some(min), Some(max)) => min == max && *min != 0,
-                _ => false,
-            }
-        } else {
-            seen_history.iter().any(|h| h.state == SeenState::Completed)
-        };
-        Ok((is_finished, seen_history))
-    }
-
-    async fn queue_notifications_to_user_platforms(
-        &self,
-        user_id: &String,
-        msg: &str,
-    ) -> Result<bool> {
-        let user_details = user_by_id(&self.db, user_id).await?;
-        if user_details.preferences.notifications.enabled {
-            let insert_data = queued_notification::ActiveModel {
-                user_id: ActiveValue::Set(user_id.to_owned()),
-                message: ActiveValue::Set(msg.to_owned()),
-                ..Default::default()
-            };
-            insert_data.insert(&self.db).await?;
-        } else {
-            ryot_log!(debug, "User has disabled notifications");
-        }
-        Ok(true)
-    }
-
     async fn get_monitored_entities(
         &self,
         entity_lot: EntityLot,
     ) -> Result<HashMap<String, HashSet<String>>> {
         let monitored_entities = MonitoredEntity::find()
             .filter(monitored_entity::Column::EntityLot.eq(entity_lot))
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         let mut monitored_by = HashMap::new();
         for entity in monitored_entities {
@@ -3617,10 +2001,10 @@ ORDER BY RANDOM() LIMIT 10;
             meta_map
         );
         for (metadata_id, to_notify) in meta_map {
-            let notifications = self.update_metadata(&metadata_id, false).await?;
+            let notifications = update_metadata(&metadata_id, false, &self.0).await?;
             for user in to_notify {
                 for notification in notifications.iter() {
-                    self.queue_media_state_changed_notification_for_user(&user, notification)
+                    queue_media_state_changed_notification_for_user(&user, notification, &self.0)
                         .await?;
                 }
             }
@@ -3642,7 +2026,7 @@ ORDER BY RANDOM() LIMIT 10;
                 .unwrap_or_default();
             for user in to_notify {
                 for notification in notifications.iter() {
-                    self.queue_media_state_changed_notification_for_user(&user, notification)
+                    queue_media_state_changed_notification_for_user(&user, notification, &self.0)
                         .await?;
                 }
             }
@@ -3650,28 +2034,6 @@ ORDER BY RANDOM() LIMIT 10;
         Ok(())
     }
 
-    async fn queue_media_state_changed_notification_for_user(
-        &self,
-        user_id: &String,
-        notification: &(String, MediaStateChanged),
-    ) -> Result<()> {
-        let (msg, change) = notification;
-        let notification_preferences = user_preferences_by_id(&self.db, user_id, &self.config)
-            .await?
-            .notifications;
-        if notification_preferences.enabled && notification_preferences.to_send.contains(change) {
-            self.queue_notifications_to_user_platforms(user_id, msg)
-                .await
-                .trace_ok();
-        } else {
-            ryot_log!(
-                debug,
-                "User id = {user_id} has disabled notifications for {change}"
-            );
-        }
-        Ok(())
-    }
-
     pub async fn genres_list(&self, input: SearchInput) -> Result<SearchResults<GenreListItem>> {
         let page: u64 = input.page.unwrap_or(1).try_into().unwrap();
         let num_items = "num_items";
@@ -3694,10 +2056,10 @@ ORDER BY RANDOM() LIMIT 10;
                 Expr::col(genre::Column::Name).into(),
             ]))
             .order_by(Expr::col(Alias::new(num_items)), Order::Desc);
-        let paginator = query
-            .clone()
-            .into_model::<GenreListItem>()
-            .paginate(&self.db, self.config.frontend.page_size.try_into().unwrap());
+        let paginator = query.clone().into_model::<GenreListItem>().paginate(
+            &self.0.db,
+            self.0.config.frontend.page_size.try_into().unwrap(),
+        );
         let ItemsAndPagesNumber {
             number_of_items,
             number_of_pages,
@@ -3745,7 +2107,7 @@ ORDER BY RANDOM() LIMIT 10;
         };
         let take = input
             .take
-            .unwrap_or(self.config.frontend.page_size.try_into().unwrap());
+            .unwrap_or(self.0.config.frontend.page_size.try_into().unwrap());
         let paginator = MetadataGroup::find()
             .select_only()
             .column(metadata_group::Column::Id)
@@ -3773,7 +2135,7 @@ ORDER BY RANDOM() LIMIT 10;
             )
             .order_by(order_by, sort_order)
             .into_tuple::<String>()
-            .paginate(&self.db, take);
+            .paginate(&self.0.db, take);
         let ItemsAndPagesNumber {
             number_of_items,
             number_of_pages,
@@ -3821,7 +2183,7 @@ ORDER BY RANDOM() LIMIT 10;
         };
         let take = input
             .take
-            .unwrap_or(self.config.frontend.page_size.try_into().unwrap());
+            .unwrap_or(self.0.config.frontend.page_size.try_into().unwrap());
         let creators_paginator = Person::find()
             .apply_if(input.search.clone().and_then(|s| s.query), |query, v| {
                 query.filter(
@@ -3854,7 +2216,7 @@ ORDER BY RANDOM() LIMIT 10;
             .group_by(person::Column::Name)
             .order_by(order_by, sort_order)
             .into_tuple::<String>()
-            .paginate(&self.db, take);
+            .paginate(&self.0.db, take);
         let ItemsAndPagesNumber {
             number_of_items,
             number_of_pages,
@@ -3878,15 +2240,15 @@ ORDER BY RANDOM() LIMIT 10;
 
     pub async fn person_details(&self, person_id: String) -> Result<PersonDetails> {
         let mut details = Person::find_by_id(person_id.clone())
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
             .unwrap();
         details.display_images =
-            metadata_images_as_urls(&details.images, &self.file_storage_service).await;
+            metadata_images_as_urls(&details.images, &self.0.file_storage_service).await;
         let associations = MetadataToPerson::find()
             .filter(metadata_to_person::Column::PersonId.eq(person_id))
             .order_by_asc(metadata_to_person::Column::Index)
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         let mut contents: HashMap<_, Vec<_>> = HashMap::new();
         for assoc in associations {
@@ -3938,12 +2300,12 @@ ORDER BY RANDOM() LIMIT 10;
     pub async fn genre_details(&self, input: GenreDetailsInput) -> Result<GenreDetails> {
         let page = input.page.unwrap_or(1);
         let genre = Genre::find_by_id(input.genre_id.clone())
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
             .unwrap();
         let paginator = MetadataToGenre::find()
             .filter(metadata_to_genre::Column::GenreId.eq(input.genre_id))
-            .paginate(&self.db, self.config.frontend.page_size as u64);
+            .paginate(&self.0.db, self.0.config.frontend.page_size as u64);
         let ItemsAndPagesNumber {
             number_of_items,
             number_of_pages,
@@ -3977,13 +2339,14 @@ ORDER BY RANDOM() LIMIT 10;
         metadata_group_id: String,
     ) -> Result<MetadataGroupDetails> {
         let mut group = MetadataGroup::find_by_id(metadata_group_id)
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
             .unwrap();
         let mut images = vec![];
         for image in group.images.iter() {
             images.push(
-                self.file_storage_service
+                self.0
+                    .file_storage_service
                     .get_stored_asset(image.url.clone())
                     .await,
             );
@@ -4017,7 +2380,7 @@ ORDER BY RANDOM() LIMIT 10;
             .filter(metadata_to_metadata_group::Column::MetadataGroupId.eq(group.id.clone()))
             .order_by_asc(metadata_to_metadata_group::Column::Part)
             .into_tuple::<String>()
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         Ok(MetadataGroupDetails {
             details: group,
@@ -4036,20 +2399,24 @@ ORDER BY RANDOM() LIMIT 10;
         for (cte, col) in CollectionToEntity::find()
             .find_also_related(Collection)
             .filter(collection::Column::Name.eq(DefaultCollection::Reminders.to_string()))
-            .all(&self.db)
+            .all(&self.0.db)
             .await?
         {
             if let Some(reminder) = cte.information {
                 let reminder: UserMediaReminder =
                     serde_json::from_str(&serde_json::to_string(&reminder)?)?;
                 let col = col.unwrap();
-                let related_users = col.find_related(UserToEntity).all(&self.db).await?;
-                if get_current_date(self.timezone.as_ref()) == reminder.reminder {
+                let related_users = col.find_related(UserToEntity).all(&self.0.db).await?;
+                if get_current_date(&self.0.timezone) == reminder.reminder {
                     for user in related_users {
-                        self.queue_notifications_to_user_platforms(&user.user_id, &reminder.text)
-                            .await?;
+                        queue_notifications_to_user_platforms(
+                            &user.user_id,
+                            &reminder.text,
+                            &self.0.db,
+                        )
+                        .await?;
                         remove_entity_from_collection(
-                            &self.db,
+                            &self.0.db,
                             &user.user_id,
                             ChangeCollectionToEntityInput {
                                 creator_user_id: col.user_id.clone(),
@@ -4073,7 +2440,7 @@ ORDER BY RANDOM() LIMIT 10;
         input: CreateReviewCommentInput,
     ) -> Result<bool> {
         let review = Review::find_by_id(input.review_id)
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
             .unwrap();
         let mut comments = review.comments.clone();
@@ -4096,7 +2463,7 @@ ORDER BY RANDOM() LIMIT 10;
                 .unwrap();
             comment.liked_by.remove(&user_id);
         } else {
-            let user = user_by_id(&self.db, &user_id).await?;
+            let user = user_by_id(&self.0.db, &user_id).await?;
             comments.push(ImportOrExportItemReviewComment {
                 id: nanoid!(20),
                 text: input.text.unwrap(),
@@ -4110,22 +2477,22 @@ ORDER BY RANDOM() LIMIT 10;
         }
         let mut review: review::ActiveModel = review.into();
         review.comments = ActiveValue::Set(comments);
-        review.update(&self.db).await?;
+        review.update(&self.0.db).await?;
         Ok(true)
     }
 
     pub async fn recalculate_calendar_events(&self) -> Result<()> {
-        let date_to_calculate_from = get_current_date(self.timezone.as_ref()).pred_opt().unwrap();
+        let date_to_calculate_from = get_current_date(&self.0.timezone).pred_opt().unwrap();
 
         let mut meta_stream = Metadata::find()
             .filter(metadata::Column::LastUpdatedOn.gte(date_to_calculate_from))
             .filter(metadata::Column::IsPartial.eq(false))
-            .stream(&self.db)
+            .stream(&self.0.db)
             .await?;
 
         while let Some(meta) = meta_stream.try_next().await? {
             ryot_log!(trace, "Processing metadata id = {:#?}", meta.id);
-            let calendar_events = meta.find_related(CalendarEvent).all(&self.db).await?;
+            let calendar_events = meta.find_related(CalendarEvent).all(&self.0.db).await?;
             for cal_event in calendar_events {
                 let mut need_to_delete = true;
                 if let Some(show) = cal_event.metadata_show_extra_information {
@@ -4172,7 +2539,7 @@ ORDER BY RANDOM() LIMIT 10;
                         cal_event.id
                     );
                     CalendarEvent::delete_by_id(cal_event.id)
-                        .exec(&self.db)
+                        .exec(&self.0.db)
                         .await?;
                 }
             }
@@ -4188,7 +2555,7 @@ ORDER BY RANDOM() LIMIT 10;
                     .or(metadata::Column::IsPartial.eq(false)),
             )
             .order_by_desc(metadata::Column::LastUpdatedOn)
-            .stream(&self.db)
+            .stream(&self.0.db)
             .await?;
         let mut calendar_events_inserts = vec![];
         let mut metadata_updates = vec![];
@@ -4248,18 +2615,18 @@ ORDER BY RANDOM() LIMIT 10;
         }
         for cal_insert in calendar_events_inserts {
             ryot_log!(debug, "Inserting calendar event: {:?}", cal_insert);
-            cal_insert.insert(&self.db).await.ok();
+            cal_insert.insert(&self.0.db).await.ok();
         }
         ryot_log!(debug, "Finished updating calendar events");
         Ok(())
     }
 
     async fn queue_notifications_for_released_media(&self) -> Result<()> {
-        let today = get_current_date(self.timezone.as_ref());
+        let today = get_current_date(&self.0.timezone);
         let calendar_events = CalendarEvent::find()
             .filter(calendar_event::Column::Date.eq(today))
             .find_also_related(Metadata)
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         let notifications = calendar_events
             .into_iter()
@@ -4290,11 +2657,10 @@ ORDER BY RANDOM() LIMIT 10;
             })
             .collect_vec();
         for (metadata_id, notification) in notifications.into_iter() {
-            let users_to_notify = self
-                .get_entities_monitored_by(&metadata_id, EntityLot::Metadata)
-                .await?;
+            let users_to_notify =
+                get_entities_monitored_by(&metadata_id, EntityLot::Metadata, &self.0.db).await?;
             for user in users_to_notify {
-                self.queue_media_state_changed_notification_for_user(&user, &notification)
+                queue_media_state_changed_notification_for_user(&user, &notification, &self.0)
                     .await?;
             }
         }
@@ -4304,7 +2670,7 @@ ORDER BY RANDOM() LIMIT 10;
     async fn update_person(&self, person_id: String) -> Result<Vec<(String, MediaStateChanged)>> {
         let mut notifications = vec![];
         let person = Person::find_by_id(person_id.clone())
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
             .unwrap();
         let provider = self.get_non_metadata_provider(person.source).await?;
@@ -4333,12 +2699,12 @@ ORDER BY RANDOM() LIMIT 10;
         to_update_person.name = ActiveValue::Set(provider_person.name);
         for (role, media) in provider_person.related.clone() {
             let title = media.title.clone();
-            let pm = self.create_partial_metadata(media).await?;
+            let pm = create_partial_metadata(media, &self.0.db).await?;
             let already_intermediate = MetadataToPerson::find()
                 .filter(metadata_to_person::Column::MetadataId.eq(&pm.id))
                 .filter(metadata_to_person::Column::PersonId.eq(&person_id))
                 .filter(metadata_to_person::Column::Role.eq(&role))
-                .one(&self.db)
+                .one(&self.0.db)
                 .await?;
             if already_intermediate.is_none() {
                 let intermediate = metadata_to_person::ActiveModel {
@@ -4347,7 +2713,7 @@ ORDER BY RANDOM() LIMIT 10;
                     role: ActiveValue::Set(role.clone()),
                     ..Default::default()
                 };
-                intermediate.insert(&self.db).await.unwrap();
+                intermediate.insert(&self.0.db).await.unwrap();
             }
             let search_for = MediaAssociatedPersonStateChanges {
                 media: CommitMediaInput {
@@ -4370,24 +2736,35 @@ ORDER BY RANDOM() LIMIT 10;
             }
         }
         to_update_person.state_changes = ActiveValue::Set(Some(default_state_changes));
-        to_update_person.update(&self.db).await.unwrap();
+        to_update_person.update(&self.0.db).await.unwrap();
         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_person_and_notify_users(&self, person_id: String) -> Result<()> {
         let notifications = self
             .update_person(person_id.clone())
             .await
             .unwrap_or_default();
         if !notifications.is_empty() {
-            let users_to_notify = self
-                .get_entities_monitored_by(&person_id, EntityLot::Person)
-                .await?;
+            let users_to_notify =
+                get_entities_monitored_by(&person_id, EntityLot::Person, &self.0.db).await?;
             for notification in notifications {
                 for user_id in users_to_notify.iter() {
-                    self.queue_media_state_changed_notification_for_user(user_id, &notification)
-                        .await
-                        .trace_ok();
+                    queue_media_state_changed_notification_for_user(
+                        user_id,
+                        &notification,
+                        &self.0,
+                    )
+                    .await
+                    .trace_ok();
                 }
             }
         }
@@ -4396,7 +2773,7 @@ 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)
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
             .unwrap();
         self.commit_metadata_group(CommitMediaInput {
@@ -4409,26 +2786,9 @@ ORDER BY RANDOM() LIMIT 10;
         Ok(())
     }
 
-    async fn get_entities_monitored_by(
-        &self,
-        entity_id: &String,
-        entity_lot: EntityLot,
-    ) -> Result<Vec<String>> {
-        let all_entities = MonitoredEntity::find()
-            .select_only()
-            .column(monitored_entity::Column::UserId)
-            .filter(monitored_entity::Column::EntityId.eq(entity_id))
-            .filter(monitored_entity::Column::EntityLot.eq(entity_lot))
-            .into_tuple::<String>()
-            .all(&self.db)
-            .await?;
-        Ok(all_entities)
-    }
-
     pub async fn handle_review_posted_event(&self, event: ReviewPostedEvent) -> Result<()> {
-        let monitored_by = self
-            .get_entities_monitored_by(&event.obj_id, event.entity_lot)
-            .await?;
+        let monitored_by =
+            get_entities_monitored_by(&event.obj_id, event.entity_lot, &self.0.db).await?;
         let users = User::find()
             .select_only()
             .column(user::Column::Id)
@@ -4438,7 +2798,7 @@ ORDER BY RANDOM() LIMIT 10;
                 MediaStateChanged::ReviewPosted
             )))
             .into_tuple::<String>()
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         for user_id in users {
             let url = self.get_entity_details_frontend_url(
@@ -4446,12 +2806,13 @@ ORDER BY RANDOM() LIMIT 10;
                 event.entity_lot,
                 Some("reviews"),
             );
-            self.queue_notifications_to_user_platforms(
+            queue_notifications_to_user_platforms(
                 &user_id,
                 &format!(
                     "New review posted for {} ({}, {}) by {}.",
                     event.obj_title, event.entity_lot, url, event.username
                 ),
+                &self.0.db,
             )
             .await?;
         }
@@ -4473,7 +2834,7 @@ ORDER BY RANDOM() LIMIT 10;
             EntityLot::MetadataGroup => format!("media/groups/item/{}", id),
             EntityLot::WorkoutTemplate => format!("fitness/templates/{}", id),
         };
-        url = format!("{}/{}", self.config.frontend.url, url);
+        url = format!("{}/{}", self.0.config.frontend.url, url);
         if let Some(tab) = default_tab {
             url += format!("?defaultTab={}", tab).as_str()
         }
@@ -4483,14 +2844,14 @@ ORDER BY RANDOM() LIMIT 10;
     async fn invalidate_import_jobs(&self) -> Result<()> {
         let all_jobs = ImportReport::find()
             .filter(import_report::Column::WasSuccess.is_null())
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         for job in all_jobs {
             if Utc::now() - job.started_on > ChronoDuration::try_hours(24).unwrap() {
                 ryot_log!(debug, "Invalidating job with id = {id}", id = job.id);
                 let mut job: import_report::ActiveModel = job.into();
                 job.was_success = ActiveValue::Set(Some(false));
-                job.save(&self.db).await?;
+                job.save(&self.0.db).await?;
             }
         }
         Ok(())
@@ -4513,18 +2874,18 @@ ORDER BY RANDOM() LIMIT 10;
             .filter(collection::Column::Name.eq(DefaultCollection::Monitoring.to_string()))
             .order_by_asc(collection_to_entity::Column::Id)
             .into_model::<CustomQueryResponse>()
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         let mut to_delete = vec![];
         for cte in all_cte {
             let delta = cte.last_updated_on.unwrap_or_else(Utc::now) - cte.created_on;
-            if delta.num_days().abs() > self.config.media.monitoring_remove_after_days {
+            if delta.num_days().abs() > self.0.config.media.monitoring_remove_after_days {
                 to_delete.push(cte.id);
             }
         }
         CollectionToEntity::delete_many()
             .filter(collection_to_entity::Column::Id.is_in(to_delete))
-            .exec(&self.db)
+            .exec(&self.0.db)
             .await?;
         Ok(())
     }
@@ -4536,11 +2897,11 @@ ORDER BY RANDOM() LIMIT 10;
             .left_join(UserToEntity)
             .filter(user_to_entity::Column::MetadataId.is_null())
             .into_tuple::<String>()
-            .stream(&self.db)
+            .stream(&self.0.db)
             .await?;
         while let Some(meta) = metadata_stream.try_next().await? {
             ryot_log!(debug, "Removing metadata id = {:#?}", meta);
-            Metadata::delete_by_id(meta).exec(&self.db).await?;
+            Metadata::delete_by_id(meta).exec(&self.0.db).await?;
         }
         let mut people_stream = Person::find()
             .select_only()
@@ -4548,11 +2909,11 @@ ORDER BY RANDOM() LIMIT 10;
             .left_join(UserToEntity)
             .filter(user_to_entity::Column::PersonId.is_null())
             .into_tuple::<String>()
-            .stream(&self.db)
+            .stream(&self.0.db)
             .await?;
         while let Some(person) = people_stream.try_next().await? {
             ryot_log!(debug, "Removing person id = {:#?}", person);
-            Person::delete_by_id(person).exec(&self.db).await?;
+            Person::delete_by_id(person).exec(&self.0.db).await?;
         }
         let mut metadata_group_stream = MetadataGroup::find()
             .select_only()
@@ -4560,12 +2921,12 @@ ORDER BY RANDOM() LIMIT 10;
             .left_join(UserToEntity)
             .filter(user_to_entity::Column::MetadataGroupId.is_null())
             .into_tuple::<String>()
-            .stream(&self.db)
+            .stream(&self.0.db)
             .await?;
         while let Some(meta_group) = metadata_group_stream.try_next().await? {
             ryot_log!(debug, "Removing metadata group id = {:#?}", meta_group);
             MetadataGroup::delete_by_id(meta_group)
-                .exec(&self.db)
+                .exec(&self.0.db)
                 .await?;
         }
         let mut genre_stream = Genre::find()
@@ -4574,23 +2935,23 @@ ORDER BY RANDOM() LIMIT 10;
             .left_join(MetadataToGenre)
             .filter(metadata_to_genre::Column::MetadataId.is_null())
             .into_tuple::<String>()
-            .stream(&self.db)
+            .stream(&self.0.db)
             .await?;
         while let Some(genre) = genre_stream.try_next().await? {
             ryot_log!(debug, "Removing genre id = {:#?}", genre);
-            Genre::delete_by_id(genre).exec(&self.db).await?;
+            Genre::delete_by_id(genre).exec(&self.0.db).await?;
         }
         ryot_log!(debug, "Deleting all queued notifications");
-        QueuedNotification::delete_many().exec(&self.db).await?;
+        QueuedNotification::delete_many().exec(&self.0.db).await?;
         ryot_log!(debug, "Deleting revoked access tokens");
         AccessLink::delete_many()
             .filter(access_link::Column::IsRevoked.eq(true))
-            .exec(&self.db)
+            .exec(&self.0.db)
             .await?;
         ryot_log!(debug, "Deleting expired application caches");
         ApplicationCache::delete_many()
             .filter(application_cache::Column::ExpiresAt.lt(Utc::now()))
-            .exec(&self.db)
+            .exec(&self.0.db)
             .await?;
         Ok(())
     }
@@ -4631,7 +2992,7 @@ ORDER BY RANDOM() LIMIT 10;
             Metadata::update_many(),
             metadata::Column::Id,
             metadata::Column::IsPartial,
-            &self.db,
+            &self.0.db,
         )
         .await?;
         update_partial_states(
@@ -4639,7 +3000,7 @@ ORDER BY RANDOM() LIMIT 10;
             MetadataGroup::update_many(),
             metadata_group::Column::Id,
             metadata_group::Column::IsPartial,
-            &self.db,
+            &self.0.db,
         )
         .await?;
         update_partial_states(
@@ -4647,19 +3008,19 @@ ORDER BY RANDOM() LIMIT 10;
             Person::update_many(),
             person::Column::Id,
             person::Column::IsPartial,
-            &self.db,
+            &self.0.db,
         )
         .await?;
         Ok(())
     }
 
     pub async fn send_pending_notifications(&self) -> Result<()> {
-        let users = User::find().all(&self.db).await?;
+        let users = User::find().all(&self.0.db).await?;
         for user_details in users {
             ryot_log!(debug, "Sending notification to user: {:?}", user_details.id);
             let notifications = QueuedNotification::find()
                 .filter(queued_notification::Column::UserId.eq(&user_details.id))
-                .all(&self.db)
+                .all(&self.0.db)
                 .await?;
             if notifications.is_empty() {
                 continue;
@@ -4671,7 +3032,7 @@ ORDER BY RANDOM() LIMIT 10;
                 .join("\n");
             let platforms = NotificationPlatform::find()
                 .filter(notification_platform::Column::UserId.eq(&user_details.id))
-                .all(&self.db)
+                .all(&self.0.db)
                 .await?;
             for notification in platforms {
                 if notification.is_disabled.unwrap_or_default() {
@@ -4684,7 +3045,7 @@ ORDER BY RANDOM() LIMIT 10;
                     continue;
                 }
                 if let Err(err) =
-                    send_notification(notification.platform_specifics, &self.config, &msg).await
+                    send_notification(notification.platform_specifics, &self.0.config, &msg).await
                 {
                     ryot_log!(trace, "Error sending notification: {:?}", err);
                 }
diff --git a/crates/services/notification/src/lib.rs b/crates/services/notification/src/lib.rs
index 9de6c6b669..8387ac2608 100644
--- a/crates/services/notification/src/lib.rs
+++ b/crates/services/notification/src/lib.rs
@@ -1,4 +1,4 @@
-use std::{env, sync::Arc};
+use std::env;
 
 use anyhow::{anyhow, Result};
 use askama::Template;
@@ -20,7 +20,7 @@ use user_models::NotificationPlatformSpecifics;
 // TODO: Allow formatting messages
 pub async fn send_notification(
     specifics: NotificationPlatformSpecifics,
-    config: &Arc<AppConfig>,
+    config: &AppConfig,
     msg: &str,
 ) -> Result<()> {
     let project_name = PROJECT_NAME.to_case(Case::Title);
diff --git a/crates/services/statistics/Cargo.toml b/crates/services/statistics/Cargo.toml
index b82333cc10..e3ffc1f337 100644
--- a/crates/services/statistics/Cargo.toml
+++ b/crates/services/statistics/Cargo.toml
@@ -10,6 +10,7 @@ database-utils = { path = "../../utils/database" }
 dependent-models = { path = "../../models/dependent" }
 media-models = { path = "../../models/media" }
 sea-orm = { workspace = true }
+supporting-service = { path = "../supporting" }
 tracing = { workspace = true }
 
 [package.metadata.cargo-machete]
diff --git a/crates/services/statistics/src/lib.rs b/crates/services/statistics/src/lib.rs
index 0657542017..0bd5425cbf 100644
--- a/crates/services/statistics/src/lib.rs
+++ b/crates/services/statistics/src/lib.rs
@@ -1,4 +1,4 @@
-use std::fmt::Write;
+use std::{fmt::Write, sync::Arc};
 
 use async_graphql::Result;
 use database_models::{daily_user_activity, prelude::DailyUserActivity};
@@ -10,20 +10,13 @@ use media_models::{
 use sea_orm::{
     prelude::Expr,
     sea_query::{Alias, Func},
-    ColumnTrait, DatabaseConnection, EntityTrait, Iden, QueryFilter, QueryOrder, QuerySelect,
-    QueryTrait,
+    ColumnTrait, EntityTrait, Iden, QueryFilter, QueryOrder, QuerySelect, QueryTrait,
 };
+use supporting_service::SupportingService;
 
-#[derive(Debug)]
-pub struct StatisticsService {
-    db: DatabaseConnection,
-}
+pub struct StatisticsService(pub Arc<SupportingService>);
 
 impl StatisticsService {
-    pub fn new(db: &DatabaseConnection) -> Self {
-        Self { db: db.clone() }
-    }
-
     pub async fn daily_user_activities(
         &self,
         user_id: &String,
@@ -58,7 +51,7 @@ impl StatisticsService {
                     "num_days",
                 )
                 .into_tuple::<Option<i32>>()
-                .one(&self.db)
+                .one(&self.0.db)
                 .await?;
             if let Some(Some(num_days)) = total {
                 if num_days >= 500 {
@@ -192,7 +185,7 @@ impl StatisticsService {
             .group_by(day_alias.clone())
             .order_by_asc(day_alias)
             .into_model::<DailyUserActivityItem>()
-            .all(&self.db)
+            .all(&self.0.db)
             .await
             .unwrap();
         let total_count = items.iter().map(|i| i.total_count).sum();
@@ -225,6 +218,6 @@ impl StatisticsService {
         user_id: &String,
         calculate_from_beginning: bool,
     ) -> Result<()> {
-        calculate_user_activities_and_summary(&self.db, user_id, calculate_from_beginning).await
+        calculate_user_activities_and_summary(&self.0.db, user_id, calculate_from_beginning).await
     }
 }
diff --git a/crates/services/supporting/Cargo.toml b/crates/services/supporting/Cargo.toml
new file mode 100644
index 0000000000..212b9efadb
--- /dev/null
+++ b/crates/services/supporting/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "supporting-service"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+apalis = { workspace = true }
+background = { path = "../../background" }
+cache-service = { path = "../../services/cache" }
+chrono-tz = { workspace = true }
+config = { path = "../../config" }
+file-storage-service = { path = "../../services/file-storage" }
+media-models = { path = "../../models/media" }
+moka = { workspace = true }
+openidconnect = { workspace = true }
+sea-orm = { workspace = true }
diff --git a/crates/services/supporting/src/lib.rs b/crates/services/supporting/src/lib.rs
new file mode 100644
index 0000000000..b0ecf3103e
--- /dev/null
+++ b/crates/services/supporting/src/lib.rs
@@ -0,0 +1,52 @@
+use std::sync::Arc;
+
+use apalis::prelude::MemoryStorage;
+use background::{ApplicationJob, CoreApplicationJob};
+use cache_service::CacheService;
+use file_storage_service::FileStorageService;
+use media_models::CommitCache;
+use moka::future::Cache;
+use openidconnect::core::CoreClient;
+use sea_orm::DatabaseConnection;
+
+pub struct SupportingService {
+    pub is_pro: bool,
+    pub db: DatabaseConnection,
+    pub timezone: chrono_tz::Tz,
+    pub cache_service: CacheService,
+    pub config: Arc<config::AppConfig>,
+    pub oidc_client: Option<CoreClient>,
+    pub commit_cache: Cache<CommitCache, ()>,
+    pub file_storage_service: Arc<FileStorageService>,
+    pub perform_application_job: MemoryStorage<ApplicationJob>,
+    pub perform_core_application_job: MemoryStorage<CoreApplicationJob>,
+}
+
+impl SupportingService {
+    #[allow(clippy::too_many_arguments)]
+    pub async fn new(
+        is_pro: bool,
+        db: &DatabaseConnection,
+        timezone: chrono_tz::Tz,
+        cache_service: CacheService,
+        config: Arc<config::AppConfig>,
+        oidc_client: Option<CoreClient>,
+        commit_cache: Cache<CommitCache, ()>,
+        file_storage_service: Arc<FileStorageService>,
+        perform_application_job: &MemoryStorage<ApplicationJob>,
+        perform_core_application_job: &MemoryStorage<CoreApplicationJob>,
+    ) -> Self {
+        Self {
+            config,
+            is_pro,
+            timezone,
+            oidc_client,
+            commit_cache,
+            cache_service,
+            db: db.clone(),
+            file_storage_service,
+            perform_application_job: perform_application_job.clone(),
+            perform_core_application_job: perform_core_application_job.clone(),
+        }
+    }
+}
diff --git a/crates/services/user/Cargo.toml b/crates/services/user/Cargo.toml
index 096492984c..5771ccfcef 100644
--- a/crates/services/user/Cargo.toml
+++ b/crates/services/user/Cargo.toml
@@ -4,15 +4,12 @@ version = "0.1.0"
 edition = "2021"
 
 [dependencies]
-apalis = { workspace = true }
 argon2 = { workspace = true }
 async-graphql = { workspace = true }
 application-utils = { path = "../../utils/application" }
-background = { path = "../../background" }
 chrono = { workspace = true }
 common-models = { path = "../../models/common" }
 common-utils = { path = "../../utils/common" }
-config = { path = "../../config" }
 database-models = { path = "../../models/database" }
 database-utils = { path = "../../utils/database" }
 dependent-models = { path = "../../models/dependent" }
@@ -27,6 +24,7 @@ notification-service = { path = "../../services/notification" }
 openidconnect = { workspace = true }
 serde_json = { workspace = true }
 sea-orm = { workspace = true }
+supporting-service = { path = "../supporting" }
 tracing = { workspace = true }
 user-models = { path = "../../models/user" }
 
diff --git a/crates/services/user/src/lib.rs b/crates/services/user/src/lib.rs
index 47d5ea9fd8..894b21e501 100644
--- a/crates/services/user/src/lib.rs
+++ b/crates/services/user/src/lib.rs
@@ -1,10 +1,8 @@
 use std::{str::FromStr, sync::Arc};
 
-use apalis::prelude::MemoryStorage;
 use application_utils::user_id_from_token;
 use argon2::{Argon2, PasswordHash, PasswordVerifier};
 use async_graphql::{Error, Result};
-use background::ApplicationJob;
 use chrono::{Timelike, Utc};
 use common_models::{DefaultCollection, StringIdObject};
 use common_utils::ryot_log;
@@ -37,16 +35,16 @@ use media_models::{
 use nanoid::nanoid;
 use notification_service::send_notification;
 use openidconnect::{
-    core::{CoreClient, CoreResponseType},
-    reqwest::async_http_client,
-    AuthenticationFlow, AuthorizationCode, CsrfToken, Nonce, Scope, TokenResponse,
+    core::CoreResponseType, reqwest::async_http_client, AuthenticationFlow, AuthorizationCode,
+    CsrfToken, Nonce, Scope, TokenResponse,
 };
 use sea_orm::{
     prelude::Expr,
     sea_query::{extension::postgres::PgExpr, Func},
-    ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, Iterable,
-    ModelTrait, Order, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, QueryTrait,
+    ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, Iterable, ModelTrait, Order,
+    PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, QueryTrait,
 };
+use supporting_service::SupportingService;
 use user_models::{
     DashboardElementLot, GridPacking, NotificationPlatformSpecifics, UserGeneralDashboardElement,
     UserGeneralPreferences, UserPreferences, UserReviewScale,
@@ -56,34 +54,11 @@ fn empty_nonce_verifier(_nonce: Option<&Nonce>) -> Result<(), String> {
     Ok(())
 }
 
-#[derive(Debug)]
-pub struct UserService {
-    is_pro: bool,
-    db: DatabaseConnection,
-    config: Arc<config::AppConfig>,
-    oidc_client: Arc<Option<CoreClient>>,
-    perform_application_job: MemoryStorage<ApplicationJob>,
-}
+pub struct UserService(pub Arc<SupportingService>);
 
 impl UserService {
-    pub fn new(
-        is_pro: bool,
-        db: &DatabaseConnection,
-        config: Arc<config::AppConfig>,
-        perform_application_job: &MemoryStorage<ApplicationJob>,
-        oidc_client: Arc<Option<CoreClient>>,
-    ) -> Self {
-        Self {
-            config,
-            is_pro,
-            oidc_client,
-            db: db.clone(),
-            perform_application_job: perform_application_job.clone(),
-        }
-    }
-
     pub async fn user_recommendations(&self, user_id: &String) -> Result<Vec<String>> {
-        let preferences = user_preferences_by_id(&self.db, user_id, &self.config).await?;
+        let preferences = user_preferences_by_id(user_id, &self.0).await?;
         let limit = preferences
             .general
             .dashboard
@@ -103,7 +78,7 @@ impl UserService {
                 Order::Desc,
             )
             .limit(limit)
-            .all(&self.db)
+            .all(&self.0.db)
             .await?
             .into_iter()
             .map(|r| r.id)
@@ -115,7 +90,7 @@ impl UserService {
         let links = AccessLink::find()
             .filter(access_link::Column::UserId.eq(user_id))
             .order_by_desc(access_link::Column::CreatedOn)
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         Ok(links)
     }
@@ -125,7 +100,7 @@ impl UserService {
         input: CreateAccessLinkInput,
         user_id: String,
     ) -> Result<StringIdObject> {
-        pro_instance_guard(self.is_pro).await?;
+        pro_instance_guard(self.0.is_pro).await?;
         let new_link = access_link::ActiveModel {
             user_id: ActiveValue::Set(user_id),
             name: ActiveValue::Set(input.name),
@@ -136,7 +111,7 @@ impl UserService {
             is_mutation_allowed: ActiveValue::Set(input.is_mutation_allowed),
             ..Default::default()
         };
-        let link = new_link.insert(&self.db).await?;
+        let link = new_link.insert(&self.0.db).await?;
         Ok(StringIdObject { id: link.id })
     }
 
@@ -145,11 +120,11 @@ impl UserService {
         input: ProcessAccessLinkInput,
     ) -> Result<ProcessAccessLinkResult> {
         let maybe_link = match input {
-            ProcessAccessLinkInput::Id(id) => AccessLink::find_by_id(id).one(&self.db).await?,
+            ProcessAccessLinkInput::Id(id) => AccessLink::find_by_id(id).one(&self.0.db).await?,
             ProcessAccessLinkInput::Username(username) => {
                 let user = User::find()
                     .filter(user::Column::Name.eq(username))
-                    .one(&self.db)
+                    .one(&self.0.db)
                     .await?;
                 match user {
                     None => None,
@@ -157,7 +132,7 @@ impl UserService {
                         u.find_related(AccessLink)
                             .filter(access_link::Column::IsAccountDefault.eq(true))
                             .filter(access_link::Column::IsRevoked.is_null())
-                            .one(&self.db)
+                            .one(&self.0.db)
                             .await?
                     }
                 }
@@ -193,11 +168,11 @@ impl UserService {
         let validity = if let Some(expires) = link.expires_on {
             (expires - Utc::now()).num_days().try_into().unwrap()
         } else {
-            self.config.users.token_valid_for_days
+            self.0.config.users.token_valid_for_days
         };
         let api_key = sign(
             link.user_id.clone(),
-            &self.config.users.jwt_secret,
+            &self.0.config.users.jwt_secret,
             validity,
             Some(AccessLinkClaims {
                 id: link.id.clone(),
@@ -208,7 +183,7 @@ impl UserService {
         issued_tokens.push(api_key.clone());
         let mut link: access_link::ActiveModel = link.into();
         link.issued_tokens = ActiveValue::Set(issued_tokens);
-        let link = link.update(&self.db).await?;
+        let link = link.update(&self.0.db).await?;
         Ok(ProcessAccessLinkResult::Ok(ProcessAccessLinkResponse {
             api_key,
             token_valid_for_days: validity,
@@ -217,8 +192,8 @@ impl UserService {
     }
 
     pub async fn revoke_access_link(&self, access_link_id: String) -> Result<bool> {
-        pro_instance_guard(self.is_pro).await?;
-        revoke_access_link(&self.db, access_link_id).await
+        pro_instance_guard(self.0.is_pro).await?;
+        revoke_access_link(&self.0.db, access_link_id).await
     }
 
     pub async fn users_list(&self, query: Option<String>) -> Result<Vec<user::Model>> {
@@ -231,7 +206,7 @@ impl UserService {
                 )
             })
             .order_by_asc(user::Column::Name)
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         Ok(users)
     }
@@ -241,8 +216,8 @@ impl UserService {
         admin_user_id: String,
         to_delete_user_id: String,
     ) -> Result<bool> {
-        admin_account_guard(&self.db, &admin_user_id).await?;
-        let maybe_user = User::find_by_id(to_delete_user_id).one(&self.db).await?;
+        admin_account_guard(&self.0.db, &admin_user_id).await?;
+        let maybe_user = User::find_by_id(to_delete_user_id).one(&self.0.db).await?;
         if let Some(u) = maybe_user {
             if self
                 .users_list(None)
@@ -256,7 +231,7 @@ impl UserService {
             {
                 return Ok(false);
             }
-            u.delete(&self.db).await?;
+            u.delete(&self.0.db).await?;
             Ok(true)
         } else {
             Ok(false)
@@ -264,8 +239,9 @@ impl UserService {
     }
 
     pub async fn register_user(&self, input: RegisterUserInput) -> Result<RegisterResult> {
-        if !self.config.users.allow_registration
-            && input.admin_access_token.unwrap_or_default() != self.config.server.admin_access_token
+        if !self.0.config.users.allow_registration
+            && input.admin_access_token.unwrap_or_default()
+                != self.0.config.server.admin_access_token
         {
             return Ok(RegisterResult::Error(RegisterError {
                 error: RegisterErrorVariant::Disabled,
@@ -283,7 +259,7 @@ impl UserService {
                 Some(data.password),
             ),
         };
-        if User::find().filter(filter).count(&self.db).await.unwrap() != 0 {
+        if User::find().filter(filter).count(&self.0.db).await.unwrap() != 0 {
             return Ok(RegisterResult::Error(RegisterError {
                 error: RegisterErrorVariant::IdentifierAlreadyExists,
             }));
@@ -292,7 +268,7 @@ impl UserService {
             AuthUserInput::Oidc(data) => Some(data.issuer_id),
             AuthUserInput::Password(_) => None,
         };
-        let lot = if User::find().count(&self.db).await.unwrap() == 0 {
+        let lot = if User::find().count(&self.0.db).await.unwrap() == 0 {
             UserLot::Admin
         } else {
             UserLot::Normal
@@ -306,7 +282,7 @@ impl UserService {
             preferences: ActiveValue::Set(UserPreferences::default()),
             ..Default::default()
         };
-        let user = user.insert(&self.db).await.unwrap();
+        let user = user.insert(&self.0.db).await.unwrap();
         ryot_log!(
             debug,
             "User {:?} registered with id {:?}",
@@ -316,7 +292,7 @@ impl UserService {
         for col in DefaultCollection::iter() {
             let meta = col.meta().to_owned();
             create_or_update_collection(
-                &self.db,
+                &self.0.db,
                 &user.id,
                 CreateOrUpdateCollectionInput {
                     name: col.to_string(),
@@ -329,7 +305,7 @@ impl UserService {
             .ok();
         }
         deploy_job_to_calculate_user_activities_and_summary(
-            &self.perform_application_job,
+            &self.0.perform_application_job,
             &user.id,
             false,
         )
@@ -340,8 +316,8 @@ impl UserService {
     pub async fn generate_auth_token(&self, user_id: String) -> Result<String> {
         let auth_token = sign(
             user_id,
-            &self.config.users.jwt_secret,
-            self.config.users.token_valid_for_days,
+            &self.0.config.users.jwt_secret,
+            self.0.config.users.token_valid_for_days,
             None,
         )?;
         Ok(auth_token)
@@ -352,7 +328,7 @@ impl UserService {
             AuthUserInput::Oidc(input) => user::Column::OidcIssuerId.eq(input.issuer_id),
             AuthUserInput::Password(input) => user::Column::Name.eq(input.username),
         };
-        match User::find().filter(filter).one(&self.db).await.unwrap() {
+        match User::find().filter(filter).one(&self.0.db).await.unwrap() {
             None => Ok(LoginResult::Error(LoginError {
                 error: LoginErrorVariant::UsernameDoesNotExist,
             })),
@@ -362,7 +338,7 @@ impl UserService {
                         error: LoginErrorVariant::AccountDisabled,
                     }));
                 }
-                if self.config.users.validate_password {
+                if self.0.config.users.validate_password {
                     if let AuthUserInput::Password(PasswordUserInput { password, .. }) = input {
                         if let Some(hashed_password) = &user.password {
                             let parsed_hash = PasswordHash::new(hashed_password).unwrap();
@@ -384,7 +360,7 @@ impl UserService {
                 let jwt_key = self.generate_auth_token(user.id.clone()).await?;
                 let mut user: user::ActiveModel = user.into();
                 user.last_login_on = ActiveValue::Set(Some(Utc::now()));
-                user.update(&self.db).await?;
+                user.update(&self.0.db).await?;
                 Ok(LoginResult::Ok(LoginResponse { api_key: jwt_key }))
             }
         }
@@ -396,12 +372,13 @@ impl UserService {
         input: UpdateUserInput,
     ) -> Result<StringIdObject> {
         if user_id.unwrap_or_default() != input.user_id
-            && input.admin_access_token.unwrap_or_default() != self.config.server.admin_access_token
+            && input.admin_access_token.unwrap_or_default()
+                != self.0.config.server.admin_access_token
         {
             return Err(Error::new("Admin access token mismatch".to_owned()));
         }
         let mut user_obj: user::ActiveModel = User::find_by_id(input.user_id)
-            .one(&self.db)
+            .one(&self.0.db)
             .await
             .unwrap()
             .unwrap()
@@ -421,7 +398,7 @@ impl UserService {
         if let Some(d) = input.is_disabled {
             user_obj.is_disabled = ActiveValue::Set(Some(d));
         }
-        let user_obj = user_obj.update(&self.db).await.unwrap();
+        let user_obj = user_obj.update(&self.0.db).await.unwrap();
         Ok(StringIdObject { id: user_obj.id })
     }
 
@@ -431,7 +408,7 @@ impl UserService {
         input: UpdateUserPreferenceInput,
     ) -> Result<bool> {
         let err = || Error::new("Incorrect property value encountered");
-        let user_model = user_by_id(&self.db, &user_id).await?;
+        let user_model = user_by_id(&self.0.db, &user_id).await?;
         let mut preferences = user_model.preferences.clone();
         match input.property.is_empty() {
             true => {
@@ -767,7 +744,7 @@ impl UserService {
         };
         let mut user_model: user::ActiveModel = user_model.into();
         user_model.preferences = ActiveValue::Set(preferences);
-        user_model.update(&self.db).await?;
+        user_model.update(&self.0.db).await?;
         Ok(true)
     }
 
@@ -777,7 +754,7 @@ impl UserService {
         input: UpdateUserIntegrationInput,
     ) -> Result<bool> {
         let db_integration = Integration::find_by_id(input.integration_id)
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
             .ok_or_else(|| Error::new("Integration with the given id does not exist"))?;
         if db_integration.user_id != user_id {
@@ -798,7 +775,7 @@ impl UserService {
         if let Some(d) = input.is_disabled {
             db_integration.is_disabled = ActiveValue::Set(Some(d));
         }
-        db_integration.update(&self.db).await?;
+        db_integration.update(&self.0.db).await?;
         Ok(true)
     }
 
@@ -826,7 +803,7 @@ impl UserService {
             provider_specifics: ActiveValue::Set(input.provider_specifics),
             ..Default::default()
         };
-        let integration = to_insert.insert(&self.db).await?;
+        let integration = to_insert.insert(&self.0.db).await?;
         Ok(StringIdObject { id: integration.id })
     }
 
@@ -836,13 +813,13 @@ impl UserService {
         integration_id: String,
     ) -> Result<bool> {
         let integration = Integration::find_by_id(integration_id)
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
             .ok_or_else(|| Error::new("Integration with the given id does not exist"))?;
         if integration.user_id != user_id {
             return Err(Error::new("Integration does not belong to the user"));
         }
-        integration.delete(&self.db).await?;
+        integration.delete(&self.0.db).await?;
         Ok(true)
     }
 
@@ -924,7 +901,7 @@ impl UserService {
             description: ActiveValue::Set(description),
             ..Default::default()
         };
-        let new_notification_id = notification.insert(&self.db).await?.id;
+        let new_notification_id = notification.insert(&self.0.db).await?.id;
         Ok(new_notification_id)
     }
 
@@ -934,7 +911,7 @@ impl UserService {
         input: UpdateUserNotificationPlatformInput,
     ) -> Result<bool> {
         let db_notification = NotificationPlatform::find_by_id(input.notification_id)
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
             .ok_or_else(|| Error::new("Notification platform with the given id does not exist"))?;
         if db_notification.user_id != user_id {
@@ -946,7 +923,7 @@ impl UserService {
         if let Some(s) = input.is_disabled {
             db_notification.is_disabled = ActiveValue::Set(Some(s));
         }
-        db_notification.update(&self.db).await?;
+        db_notification.update(&self.0.db).await?;
         Ok(true)
     }
 
@@ -956,7 +933,7 @@ impl UserService {
         notification_id: String,
     ) -> Result<bool> {
         let notification = NotificationPlatform::find_by_id(notification_id)
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
             .ok_or_else(|| Error::new("Notification platform with the given id does not exist"))?;
         if notification.user_id != user_id {
@@ -964,33 +941,33 @@ impl UserService {
                 "Notification platform does not belong to the user",
             ));
         }
-        notification.delete(&self.db).await?;
+        notification.delete(&self.0.db).await?;
         Ok(true)
     }
 
     pub async fn test_user_notification_platforms(&self, user_id: &String) -> Result<bool> {
         let notifications = NotificationPlatform::find()
             .filter(notification_platform::Column::UserId.eq(user_id))
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         for platform in notifications {
             if platform.is_disabled.unwrap_or_default() {
                 continue;
             }
             let msg = format!("This is a test notification for platform: {}", platform.lot);
-            send_notification(platform.platform_specifics, &self.config, &msg).await?;
+            send_notification(platform.platform_specifics, &self.0.config, &msg).await?;
         }
         Ok(true)
     }
 
     pub async fn user_preferences(&self, user_id: &String) -> Result<UserPreferences> {
-        user_preferences_by_id(&self.db, user_id, &self.config).await
+        user_preferences_by_id(user_id, &self.0).await
     }
 
     pub async fn user_details(&self, token: &str) -> Result<UserDetailsResult> {
-        let found_token = user_id_from_token(token, &self.config.users.jwt_secret);
+        let found_token = user_id_from_token(token, &self.0.config.users.jwt_secret);
         if let Ok(user_id) = found_token {
-            let user = user_by_id(&self.db, &user_id).await?;
+            let user = user_by_id(&self.0.db, &user_id).await?;
             Ok(UserDetailsResult::Ok(Box::new(user)))
         } else {
             Ok(UserDetailsResult::Error(UserDetailsError {
@@ -1002,7 +979,7 @@ impl UserService {
     pub async fn user_integrations(&self, user_id: &String) -> Result<Vec<integration::Model>> {
         let integrations = Integration::find()
             .filter(integration::Column::UserId.eq(user_id))
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         Ok(integrations)
     }
@@ -1013,13 +990,13 @@ impl UserService {
     ) -> Result<Vec<notification_platform::Model>> {
         let all_notifications = NotificationPlatform::find()
             .filter(notification_platform::Column::UserId.eq(user_id))
-            .all(&self.db)
+            .all(&self.0.db)
             .await?;
         Ok(all_notifications)
     }
 
     pub async fn get_oidc_redirect_url(&self) -> Result<String> {
-        match self.oidc_client.as_ref() {
+        match self.0.oidc_client.as_ref() {
             Some(client) => {
                 let (authorize_url, _, _) = client
                     .authorize_url(
@@ -1036,7 +1013,7 @@ impl UserService {
     }
 
     pub async fn get_oidc_token(&self, code: String) -> Result<OidcTokenOutput> {
-        match self.oidc_client.as_ref() {
+        match self.0.oidc_client.as_ref() {
             Some(client) => {
                 let token = client
                     .exchange_code(AuthorizationCode::new(code))
@@ -1058,7 +1035,7 @@ impl UserService {
     pub async fn user_by_oidc_issuer_id(&self, oidc_issuer_id: String) -> Result<Option<String>> {
         let user = User::find()
             .filter(user::Column::OidcIssuerId.eq(oidc_issuer_id))
-            .one(&self.db)
+            .one(&self.0.db)
             .await?
             .map(|u| u.id);
         Ok(user)
diff --git a/crates/utils/application/src/lib.rs b/crates/utils/application/src/lib.rs
index e54aac4f68..b592aa648e 100644
--- a/crates/utils/application/src/lib.rs
+++ b/crates/utils/application/src/lib.rs
@@ -60,7 +60,7 @@ where
 pub trait GraphqlRepresentation {
     async fn graphql_representation(
         self,
-        file_storage_service: &Arc<FileStorageService>,
+        file_storage_service: &FileStorageService,
     ) -> Result<Self>
     where
         Self: Sized;
diff --git a/crates/utils/database/Cargo.toml b/crates/utils/database/Cargo.toml
index 5520266f6c..a9d376194b 100644
--- a/crates/utils/database/Cargo.toml
+++ b/crates/utils/database/Cargo.toml
@@ -11,11 +11,9 @@ background = { path = "../../background" }
 chrono = { workspace = true }
 common-models = { path = "../../models/common" }
 common-utils = { path = "../../utils/common" }
-config = { path = "../../config" }
 database-models = { path = "../../models/database" }
 dependent-models = { path = "../../models/dependent" }
 enums = { path = "../../enums" }
-file-storage-service = { path = "../../services/file-storage" }
 fitness-models = { path = "../../models/fitness" }
 futures = { workspace = true }
 itertools = { workspace = true }
@@ -27,6 +25,7 @@ rust_decimal = { workspace = true }
 rust_decimal_macros = { workspace = true }
 sea-orm = { workspace = true }
 serde = { workspace = true }
+supporting-service = { path = "../../services/supporting" }
 tracing = { workspace = true }
 user-models = { path = "../../models/user" }
 uuid = { workspace = true }
diff --git a/crates/utils/database/src/lib.rs b/crates/utils/database/src/lib.rs
index 299ab56946..f0d97bce87 100644
--- a/crates/utils/database/src/lib.rs
+++ b/crates/utils/database/src/lib.rs
@@ -22,7 +22,6 @@ use database_models::{
 };
 use dependent_models::{UserWorkoutDetails, UserWorkoutTemplateDetails};
 use enums::{EntityLot, MediaLot, SeenState, UserLot, Visibility};
-use file_storage_service::FileStorageService;
 use fitness_models::UserMeasurementsListInput;
 use futures::TryStreamExt;
 use itertools::Itertools;
@@ -45,6 +44,7 @@ use sea_orm::{
     TransactionTrait,
 };
 use serde::{Deserialize, Serialize};
+use supporting_service::SupportingService;
 use user_models::{UserPreferences, UserReviewScale};
 use uuid::Uuid;
 
@@ -72,27 +72,26 @@ pub async fn user_by_id(db: &DatabaseConnection, user_id: &String) -> Result<use
 }
 
 pub async fn user_preferences_by_id(
-    db: &DatabaseConnection,
     user_id: &String,
-    config: &Arc<config::AppConfig>,
+    ss: &Arc<SupportingService>,
 ) -> Result<UserPreferences> {
-    let mut preferences = user_by_id(db, user_id).await?.preferences;
+    let mut preferences = user_by_id(&ss.db, user_id).await?.preferences;
     preferences.features_enabled.media.anime =
-        config.anime_and_manga.is_enabled() && preferences.features_enabled.media.anime;
+        ss.config.anime_and_manga.is_enabled() && preferences.features_enabled.media.anime;
     preferences.features_enabled.media.audio_book =
-        config.audio_books.is_enabled() && preferences.features_enabled.media.audio_book;
+        ss.config.audio_books.is_enabled() && preferences.features_enabled.media.audio_book;
     preferences.features_enabled.media.book =
-        config.books.is_enabled() && preferences.features_enabled.media.book;
+        ss.config.books.is_enabled() && preferences.features_enabled.media.book;
     preferences.features_enabled.media.show =
-        config.movies_and_shows.is_enabled() && preferences.features_enabled.media.show;
+        ss.config.movies_and_shows.is_enabled() && preferences.features_enabled.media.show;
     preferences.features_enabled.media.manga =
-        config.anime_and_manga.is_enabled() && preferences.features_enabled.media.manga;
+        ss.config.anime_and_manga.is_enabled() && preferences.features_enabled.media.manga;
     preferences.features_enabled.media.movie =
-        config.movies_and_shows.is_enabled() && preferences.features_enabled.media.movie;
+        ss.config.movies_and_shows.is_enabled() && preferences.features_enabled.media.movie;
     preferences.features_enabled.media.podcast =
-        config.podcasts.is_enabled() && preferences.features_enabled.media.podcast;
+        ss.config.podcasts.is_enabled() && preferences.features_enabled.media.podcast;
     preferences.features_enabled.media.video_game =
-        config.video_games.is_enabled() && preferences.features_enabled.media.video_game;
+        ss.config.video_games.is_enabled() && preferences.features_enabled.media.video_game;
     Ok(preferences)
 }
 
@@ -189,14 +188,13 @@ pub async fn entity_in_collections(
 }
 
 pub async fn workout_details(
-    db: &DatabaseConnection,
-    file_storage_service: &Arc<FileStorageService>,
     user_id: &String,
     workout_id: String,
+    ss: &Arc<SupportingService>,
 ) -> Result<UserWorkoutDetails> {
     let maybe_workout = Workout::find_by_id(workout_id.clone())
         .filter(workout::Column::UserId.eq(user_id))
-        .one(db)
+        .one(&ss.db)
         .await?;
     match maybe_workout {
         None => Err(Error::new(
@@ -204,8 +202,8 @@ pub async fn workout_details(
         )),
         Some(e) => {
             let collections =
-                entity_in_collections(db, user_id, &workout_id, EntityLot::Workout).await?;
-            let details = e.graphql_representation(file_storage_service).await?;
+                entity_in_collections(&ss.db, user_id, &workout_id, EntityLot::Workout).await?;
+            let details = e.graphql_representation(&ss.file_storage_service).await?;
             Ok(UserWorkoutDetails {
                 details,
                 collections,
@@ -284,32 +282,31 @@ where
 }
 
 pub async fn add_entity_to_collection(
-    db: &DatabaseConnection,
     user_id: &String,
     input: ChangeCollectionToEntityInput,
-    perform_core_application_job: &MemoryStorage<CoreApplicationJob>,
+    ss: &Arc<SupportingService>,
 ) -> Result<bool> {
     let collection = Collection::find()
         .left_join(UserToEntity)
         .filter(user_to_entity::Column::UserId.eq(user_id))
         .filter(collection::Column::Name.eq(input.collection_name))
-        .one(db)
+        .one(&ss.db)
         .await
         .unwrap()
         .unwrap();
     let mut updated: collection::ActiveModel = collection.into();
     updated.last_updated_on = ActiveValue::Set(Utc::now());
-    let collection = updated.update(db).await.unwrap();
+    let collection = updated.update(&ss.db).await.unwrap();
     let column = get_cte_column_from_lot(input.entity_lot);
     let resp = if let Some(etc) = CollectionToEntity::find()
         .filter(CteColAlias::CollectionId.eq(collection.id.clone()))
         .filter(column.eq(input.entity_id.clone()))
-        .one(db)
+        .one(&ss.db)
         .await?
     {
         let mut to_update: collection_to_entity::ActiveModel = etc.into();
         to_update.last_updated_on = ActiveValue::Set(Utc::now());
-        to_update.update(db).await?
+        to_update.update(&ss.db).await?
     } else {
         let mut created_collection = collection_to_entity::ActiveModel {
             collection_id: ActiveValue::Set(collection.id),
@@ -330,17 +327,17 @@ pub async fn add_entity_to_collection(
             }
             EntityLot::Collection => unreachable!(),
         }
-        let created = created_collection.insert(db).await?;
+        let created = created_collection.insert(&ss.db).await?;
         ryot_log!(debug, "Created collection to entity: {:?}", created);
         if input.entity_lot != EntityLot::Workout && input.entity_lot != EntityLot::WorkoutTemplate
         {
-            associate_user_with_entity(db, user_id, input.entity_id, input.entity_lot)
+            associate_user_with_entity(&ss.db, user_id, input.entity_id, input.entity_lot)
                 .await
                 .ok();
         }
         created
     };
-    perform_core_application_job
+    ss.perform_core_application_job
         .enqueue(CoreApplicationJob::EntityAddedToCollection(
             user_id.to_owned(),
             resp.id,
@@ -818,8 +815,8 @@ pub async fn deploy_job_to_calculate_user_activities_and_summary(
 }
 
 pub async fn deploy_job_to_re_evaluate_user_workouts(
-    perform_application_job: &MemoryStorage<ApplicationJob>,
     user_id: &String,
+    perform_application_job: &MemoryStorage<ApplicationJob>,
 ) {
     perform_application_job
         .clone()
diff --git a/crates/utils/dependent/Cargo.toml b/crates/utils/dependent/Cargo.toml
new file mode 100644
index 0000000000..cff6004008
--- /dev/null
+++ b/crates/utils/dependent/Cargo.toml
@@ -0,0 +1,37 @@
+[package]
+name = "dependent-utils"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+anyhow = { workspace = true }
+apalis = { workspace = true }
+application-utils = { path = "../application" }
+async-graphql = { workspace = true }
+background = { path = "../../background" }
+chrono = { workspace = true }
+chrono-tz = { workspace = true }
+common-models = { path = "../../models/common" }
+common-utils = { path = "../common" }
+config = { path = "../../config" }
+database-models = { path = "../../models/database" }
+database-utils = { path = "../database" }
+dependent-models = { path = "../../models/dependent" }
+enums = { path = "../../enums" }
+fitness-models = { path = "../../models/fitness" }
+importer-models = { path = "../../models/importer" }
+itertools = { workspace = true }
+media-models = { path = "../../models/media" }
+nanoid = { workspace = true }
+providers = { path = "../../providers" }
+rust_decimal = { workspace = true }
+rust_decimal_macros = { workspace = true }
+sea-orm = { workspace = true }
+serde = { workspace = true }
+supporting-service = { path = "../../services/supporting" }
+tracing = { workspace = true }
+traits = { path = "../../traits" }
+user-models = { path = "../../models/user" }
+
+[package.metadata.cargo-machete]
+ignored = ["tracing"]
diff --git a/crates/utils/dependent/src/lib.rs b/crates/utils/dependent/src/lib.rs
new file mode 100644
index 0000000000..a3602399f6
--- /dev/null
+++ b/crates/utils/dependent/src/lib.rs
@@ -0,0 +1,2193 @@
+use std::{collections::HashMap, iter::zip, sync::Arc};
+
+use anyhow::{bail, Result as AnyhowResult};
+use apalis::prelude::{MemoryStorage, MessageQueue};
+use application_utils::get_current_date;
+use async_graphql::{Enum, Error, Result};
+use background::{ApplicationJob, CoreApplicationJob};
+use chrono::Utc;
+use common_models::{
+    ApplicationCacheKey, BackgroundJob, ChangeCollectionToEntityInput, DefaultCollection,
+    MediaStateChanged, StoredUrl, StringIdObject,
+};
+use common_utils::{ryot_log, SHOW_SPECIAL_SEASON_NAMES};
+use database_models::{
+    genre, metadata, metadata_group, metadata_to_genre, metadata_to_metadata, metadata_to_person,
+    monitored_entity, person,
+    prelude::{
+        Collection, Exercise, Genre, Metadata, MetadataGroup, MetadataToGenre, MetadataToMetadata,
+        MetadataToPerson, MonitoredEntity, Person, Seen, UserToEntity, Workout,
+    },
+    queued_notification, review, seen, user_measurement, user_to_entity, workout,
+};
+use database_utils::{
+    add_entity_to_collection, admin_account_guard, create_or_update_collection,
+    deploy_job_to_re_evaluate_user_workouts, remove_entity_from_collection, user_by_id,
+    user_preferences_by_id,
+};
+use dependent_models::ImportResult;
+use enums::{EntityLot, MediaLot, MediaSource, MetadataToMetadataRelation, SeenState, Visibility};
+use fitness_models::{
+    ExerciseBestSetRecord, ProcessedExercise, UserExerciseInput,
+    UserToExerciseBestSetExtraInformation, UserToExerciseExtraInformation,
+    UserToExerciseHistoryExtraInformation, UserWorkoutInput, UserWorkoutSetRecord,
+    WorkoutInformation, WorkoutOrExerciseTotals, WorkoutSetPersonalBest, WorkoutSetRecord,
+    WorkoutSetTotals, WorkoutSummary, WorkoutSummaryExercise, LOT_MAPPINGS,
+};
+use importer_models::{ImportDetails, ImportFailStep, ImportFailedItem, ImportResultResponse};
+use itertools::Itertools;
+use media_models::{
+    CommitCache, CommitMediaInput, CommitPersonInput, CreateOrUpdateCollectionInput,
+    CreateOrUpdateReviewInput, ImportOrExportItemRating, MediaDetails, MetadataImage,
+    PartialMetadata, PartialMetadataPerson, PartialMetadataWithoutId, ProgressUpdateError,
+    ProgressUpdateErrorVariant, ProgressUpdateInput, ProgressUpdateResultUnion, ReviewPostedEvent,
+    SeenAnimeExtraInformation, SeenMangaExtraInformation, SeenPodcastExtraInformation,
+    SeenShowExtraInformation,
+};
+use nanoid::nanoid;
+use providers::{
+    anilist::{AnilistAnimeService, AnilistMangaService},
+    audible::AudibleService,
+    google_books::GoogleBooksService,
+    igdb::IgdbService,
+    itunes::ITunesService,
+    listennotes::ListennotesService,
+    mal::{MalAnimeService, MalMangaService},
+    manga_updates::MangaUpdatesService,
+    openlibrary::OpenlibraryService,
+    tmdb::{NonMediaTmdbService, TmdbMovieService, TmdbShowService},
+    vndb::VndbService,
+};
+use rust_decimal::{
+    prelude::{One, ToPrimitive},
+    Decimal,
+};
+use rust_decimal_macros::dec;
+use sea_orm::{
+    prelude::{DateTimeUtc, Expr},
+    ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait,
+    QueryFilter, QueryOrder, QuerySelect, QueryTrait,
+};
+use serde::{Deserialize, Serialize};
+use supporting_service::SupportingService;
+use traits::{MediaProvider, TraceOk};
+use user_models::{UserPreferences, UserReviewScale};
+
+pub type Provider = Box<(dyn MediaProvider + Send + Sync)>;
+
+pub async fn get_openlibrary_service(config: &config::AppConfig) -> Result<OpenlibraryService> {
+    Ok(OpenlibraryService::new(&config.books.openlibrary, config.frontend.page_size).await)
+}
+
+pub async fn get_isbn_service(config: &config::AppConfig) -> Result<GoogleBooksService> {
+    Ok(GoogleBooksService::new(&config.books.google_books, config.frontend.page_size).await)
+}
+
+pub async fn get_tmdb_non_media_service(
+    ss: &Arc<SupportingService>,
+) -> Result<NonMediaTmdbService> {
+    Ok(NonMediaTmdbService::new(
+        &ss.config.movies_and_shows.tmdb.access_token,
+        ss.config.movies_and_shows.tmdb.locale.clone(),
+        Arc::new(ss.timezone),
+    )
+    .await)
+}
+
+pub async fn get_metadata_provider(
+    lot: MediaLot,
+    source: MediaSource,
+    ss: &Arc<SupportingService>,
+) -> Result<Provider> {
+    let err = || Err(Error::new("This source is not supported".to_owned()));
+    let service: Provider = match source {
+        MediaSource::Vndb => {
+            Box::new(VndbService::new(&ss.config.visual_novels, ss.config.frontend.page_size).await)
+        }
+        MediaSource::Openlibrary => Box::new(get_openlibrary_service(&ss.config).await?),
+        MediaSource::Itunes => Box::new(
+            ITunesService::new(&ss.config.podcasts.itunes, ss.config.frontend.page_size).await,
+        ),
+        MediaSource::GoogleBooks => Box::new(get_isbn_service(&ss.config).await?),
+        MediaSource::Audible => Box::new(
+            AudibleService::new(&ss.config.audio_books.audible, ss.config.frontend.page_size).await,
+        ),
+        MediaSource::Listennotes => Box::new(
+            ListennotesService::new(&ss.config.podcasts, ss.config.frontend.page_size).await,
+        ),
+        MediaSource::Tmdb => match lot {
+            MediaLot::Show => Box::new(
+                TmdbShowService::new(
+                    &ss.config.movies_and_shows.tmdb,
+                    Arc::new(ss.timezone),
+                    ss.config.frontend.page_size,
+                )
+                .await,
+            ),
+            MediaLot::Movie => Box::new(
+                TmdbMovieService::new(
+                    &ss.config.movies_and_shows.tmdb,
+                    Arc::new(ss.timezone),
+                    ss.config.frontend.page_size,
+                )
+                .await,
+            ),
+            _ => return err(),
+        },
+        MediaSource::Anilist => match lot {
+            MediaLot::Anime => Box::new(
+                AnilistAnimeService::new(
+                    &ss.config.anime_and_manga.anilist,
+                    ss.config.frontend.page_size,
+                )
+                .await,
+            ),
+            MediaLot::Manga => Box::new(
+                AnilistMangaService::new(
+                    &ss.config.anime_and_manga.anilist,
+                    ss.config.frontend.page_size,
+                )
+                .await,
+            ),
+            _ => return err(),
+        },
+        MediaSource::Mal => match lot {
+            MediaLot::Anime => Box::new(
+                MalAnimeService::new(&ss.config.anime_and_manga.mal, ss.config.frontend.page_size)
+                    .await,
+            ),
+            MediaLot::Manga => Box::new(
+                MalMangaService::new(&ss.config.anime_and_manga.mal, ss.config.frontend.page_size)
+                    .await,
+            ),
+            _ => return err(),
+        },
+        MediaSource::Igdb => {
+            Box::new(IgdbService::new(&ss.config.video_games, ss.config.frontend.page_size).await)
+        }
+        MediaSource::MangaUpdates => Box::new(
+            MangaUpdatesService::new(
+                &ss.config.anime_and_manga.manga_updates,
+                ss.config.frontend.page_size,
+            )
+            .await,
+        ),
+        MediaSource::Custom => return err(),
+    };
+    Ok(service)
+}
+
+pub async fn details_from_provider(
+    lot: MediaLot,
+    source: MediaSource,
+    identifier: &str,
+    ss: &Arc<SupportingService>,
+) -> Result<MediaDetails> {
+    let provider = get_metadata_provider(lot, source, ss).await?;
+    let results = provider.metadata_details(identifier).await?;
+    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,
+    index: usize,
+    db: &DatabaseConnection,
+) -> Result<()> {
+    let role = person.role.clone();
+    let db_person = commit_person(
+        CommitPersonInput {
+            identifier: person.identifier.clone(),
+            source: person.source,
+            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())),
+        character: ActiveValue::Set(person.character),
+    };
+    intermediate.insert(db).await.ok();
+    Ok(())
+}
+
+async fn associate_genre_with_metadata(
+    name: String,
+    metadata_id: &str,
+    db: &DatabaseConnection,
+) -> Result<()> {
+    let db_genre = if let Some(c) = Genre::find()
+        .filter(genre::Column::Name.eq(&name))
+        .one(db)
+        .await
+        .unwrap()
+    {
+        c
+    } else {
+        let c = genre::ActiveModel {
+            name: ActiveValue::Set(name),
+            ..Default::default()
+        };
+        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),
+    };
+    intermediate.insert(db).await.ok();
+    Ok(())
+}
+
+pub async fn create_partial_metadata(
+    data: PartialMetadataWithoutId,
+    db: &DatabaseConnection,
+) -> Result<PartialMetadata> {
+    let mode = if let Some(c) = Metadata::find()
+        .filter(metadata::Column::Identifier.eq(&data.identifier))
+        .filter(metadata::Column::Lot.eq(data.lot))
+        .filter(metadata::Column::Source.eq(data.source))
+        .one(db)
+        .await
+        .unwrap()
+    {
+        c
+    } else {
+        let image = data.image.clone().map(|i| {
+            vec![MetadataImage {
+                url: StoredUrl::Url(i),
+            }]
+        });
+        let c = metadata::ActiveModel {
+            title: ActiveValue::Set(data.title),
+            identifier: ActiveValue::Set(data.identifier),
+            lot: ActiveValue::Set(data.lot),
+            source: ActiveValue::Set(data.source),
+            images: ActiveValue::Set(image),
+            is_partial: ActiveValue::Set(Some(true)),
+            ..Default::default()
+        };
+        c.insert(db).await?
+    };
+    let model = PartialMetadata {
+        id: mode.id,
+        lot: mode.lot,
+        image: data.image,
+        title: mode.title,
+        source: mode.source,
+        identifier: mode.identifier,
+        is_recommendation: mode.is_recommendation,
+    };
+    Ok(model)
+}
+
+async fn associate_suggestion_with_metadata(
+    data: PartialMetadataWithoutId,
+    metadata_id: &str,
+    db: &DatabaseConnection,
+) -> 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),
+        relation: ActiveValue::Set(MetadataToMetadataRelation::Suggestion),
+        ..Default::default()
+    };
+    intermediate.insert(db).await.ok();
+    Ok(())
+}
+
+async fn deploy_associate_group_with_metadata_job(
+    lot: MediaLot,
+    source: MediaSource,
+    identifier: String,
+    perform_application_job: &MemoryStorage<ApplicationJob>,
+) -> Result<()> {
+    perform_application_job
+        .clone()
+        .enqueue(ApplicationJob::AssociateGroupWithMetadata(
+            lot, source, identifier,
+        ))
+        .await
+        .unwrap();
+    Ok(())
+}
+
+#[allow(clippy::too_many_arguments)]
+async fn change_metadata_associations(
+    metadata_id: &String,
+    lot: MediaLot,
+    source: MediaSource,
+    genres: Vec<String>,
+    suggestions: Vec<PartialMetadataWithoutId>,
+    groups: Vec<String>,
+    people: Vec<PartialMetadataPerson>,
+    ss: &Arc<SupportingService>,
+) -> Result<()> {
+    MetadataToPerson::delete_many()
+        .filter(metadata_to_person::Column::MetadataId.eq(metadata_id))
+        .exec(&ss.db)
+        .await?;
+    MetadataToGenre::delete_many()
+        .filter(metadata_to_genre::Column::MetadataId.eq(metadata_id))
+        .exec(&ss.db)
+        .await?;
+    MetadataToMetadata::delete_many()
+        .filter(metadata_to_metadata::Column::FromMetadataId.eq(metadata_id))
+        .filter(metadata_to_metadata::Column::Relation.eq(MetadataToMetadataRelation::Suggestion))
+        .exec(&ss.db)
+        .await?;
+    for (index, creator) in people.into_iter().enumerate() {
+        associate_person_with_metadata(metadata_id, creator, index, &ss.db)
+            .await
+            .ok();
+    }
+    for genre in genres {
+        associate_genre_with_metadata(genre, metadata_id, &ss.db)
+            .await
+            .ok();
+    }
+    for suggestion in suggestions {
+        associate_suggestion_with_metadata(suggestion, metadata_id, &ss.db)
+            .await
+            .ok();
+    }
+    for group_identifier in groups {
+        deploy_associate_group_with_metadata_job(
+            lot,
+            source,
+            group_identifier,
+            &ss.perform_application_job,
+        )
+        .await
+        .ok();
+    }
+    Ok(())
+}
+
+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)
+        .one(&ss.db)
+        .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))
+        .col_expr(metadata::Column::IsPartial, Expr::value(false))
+        .exec(&ss.db)
+        .await?;
+    let maybe_details =
+        details_from_provider(metadata.lot, metadata.source, &metadata.identifier, ss).await;
+    let notifications = match maybe_details {
+        Ok(details) => {
+            let mut notifications = vec![];
+
+            let meta = Metadata::find_by_id(metadata_id)
+                .one(&ss.db)
+                .await
+                .unwrap()
+                .unwrap();
+
+            if let (Some(p1), Some(p2)) = (&meta.production_status, &details.production_status) {
+                if p1 != p2 {
+                    notifications.push((
+                        format!("Status changed from {:#?} to {:#?}", p1, p2),
+                        MediaStateChanged::MetadataStatusChanged,
+                    ));
+                }
+            }
+            if let (Some(p1), Some(p2)) = (meta.publish_year, details.publish_year) {
+                if p1 != p2 {
+                    notifications.push((
+                        format!("Publish year from {:#?} to {:#?}", p1, p2),
+                        MediaStateChanged::MetadataReleaseDateChanged,
+                    ));
+                }
+            }
+            if let (Some(s1), Some(s2)) = (&meta.show_specifics, &details.show_specifics) {
+                if s1.seasons.len() != s2.seasons.len() {
+                    notifications.push((
+                        format!(
+                            "Number of seasons changed from {:#?} to {:#?}",
+                            s1.seasons.len(),
+                            s2.seasons.len()
+                        ),
+                        MediaStateChanged::MetadataNumberOfSeasonsChanged,
+                    ));
+                } else {
+                    for (s1, s2) in zip(s1.seasons.iter(), s2.seasons.iter()) {
+                        if SHOW_SPECIAL_SEASON_NAMES.contains(&s1.name.as_str())
+                            && SHOW_SPECIAL_SEASON_NAMES.contains(&s2.name.as_str())
+                        {
+                            continue;
+                        }
+                        if s1.episodes.len() != s2.episodes.len() {
+                            notifications.push((
+                                format!(
+                                    "Number of episodes changed from {:#?} to {:#?} (Season {})",
+                                    s1.episodes.len(),
+                                    s2.episodes.len(),
+                                    s1.season_number
+                                ),
+                                MediaStateChanged::MetadataEpisodeReleased,
+                            ));
+                        } else {
+                            for (before_episode, after_episode) in
+                                zip(s1.episodes.iter(), s2.episodes.iter())
+                            {
+                                if before_episode.name != after_episode.name {
+                                    notifications.push((
+                                        format!(
+                                            "Episode name changed from {:#?} to {:#?} (S{}E{})",
+                                            before_episode.name,
+                                            after_episode.name,
+                                            s1.season_number,
+                                            before_episode.episode_number
+                                        ),
+                                        MediaStateChanged::MetadataEpisodeNameChanged,
+                                    ));
+                                }
+                                if before_episode.poster_images != after_episode.poster_images {
+                                    notifications.push((
+                                        format!(
+                                            "Episode image changed for S{}E{}",
+                                            s1.season_number, before_episode.episode_number
+                                        ),
+                                        MediaStateChanged::MetadataEpisodeImagesChanged,
+                                    ));
+                                }
+                                if let (Some(pd1), Some(pd2)) =
+                                    (before_episode.publish_date, after_episode.publish_date)
+                                {
+                                    if pd1 != pd2 {
+                                        notifications.push((
+                                            format!(
+                                                "Episode release date changed from {:?} to {:?} (S{}E{})",
+                                                pd1,
+                                                pd2,
+                                                s1.season_number,
+                                                before_episode.episode_number
+                                            ),
+                                            MediaStateChanged::MetadataReleaseDateChanged,
+                                        ));
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            };
+            if let (Some(a1), Some(a2)) = (&meta.anime_specifics, &details.anime_specifics) {
+                if let (Some(e1), Some(e2)) = (a1.episodes, a2.episodes) {
+                    if e1 != e2 {
+                        notifications.push((
+                            format!("Number of episodes changed from {:#?} to {:#?}", e1, e2),
+                            MediaStateChanged::MetadataChaptersOrEpisodesChanged,
+                        ));
+                    }
+                }
+            };
+            if let (Some(m1), Some(m2)) = (&meta.manga_specifics, &details.manga_specifics) {
+                if let (Some(c1), Some(c2)) = (m1.chapters, m2.chapters) {
+                    if c1 != c2 {
+                        notifications.push((
+                            format!("Number of chapters changed from {:#?} to {:#?}", c1, c2),
+                            MediaStateChanged::MetadataChaptersOrEpisodesChanged,
+                        ));
+                    }
+                }
+            };
+            if let (Some(p1), Some(p2)) = (&meta.podcast_specifics, &details.podcast_specifics) {
+                if p1.episodes.len() != p2.episodes.len() {
+                    notifications.push((
+                        format!(
+                            "Number of episodes changed from {:#?} to {:#?}",
+                            p1.episodes.len(),
+                            p2.episodes.len()
+                        ),
+                        MediaStateChanged::MetadataEpisodeReleased,
+                    ));
+                } else {
+                    for (before_episode, after_episode) in
+                        zip(p1.episodes.iter(), p2.episodes.iter())
+                    {
+                        if before_episode.title != after_episode.title {
+                            notifications.push((
+                                format!(
+                                    "Episode name changed from {:#?} to {:#?} (EP{})",
+                                    before_episode.title,
+                                    after_episode.title,
+                                    before_episode.number
+                                ),
+                                MediaStateChanged::MetadataEpisodeNameChanged,
+                            ));
+                        }
+                        if before_episode.thumbnail != after_episode.thumbnail {
+                            notifications.push((
+                                format!("Episode image changed for EP{}", before_episode.number),
+                                MediaStateChanged::MetadataEpisodeImagesChanged,
+                            ));
+                        }
+                    }
+                }
+            };
+
+            let notifications = notifications
+                .into_iter()
+                .map(|n| (format!("{} for {:?}.", n.0, meta.title), n.1))
+                .collect_vec();
+
+            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 free_creators = if details.creators.is_empty() {
+                None
+            } else {
+                Some(details.creators)
+            };
+            let watch_providers = if details.watch_providers.is_empty() {
+                None
+            } else {
+                Some(details.watch_providers)
+            };
+
+            let mut meta: metadata::ActiveModel = meta.into();
+            meta.last_updated_on = ActiveValue::Set(Utc::now());
+            meta.title = ActiveValue::Set(details.title);
+            meta.is_nsfw = ActiveValue::Set(details.is_nsfw);
+            meta.is_partial = ActiveValue::Set(Some(false));
+            meta.provider_rating = ActiveValue::Set(details.provider_rating);
+            meta.description = ActiveValue::Set(details.description);
+            meta.images = ActiveValue::Set(Some(images));
+            meta.videos = ActiveValue::Set(Some(details.videos));
+            meta.production_status = ActiveValue::Set(details.production_status);
+            meta.original_language = ActiveValue::Set(details.original_language);
+            meta.publish_year = ActiveValue::Set(details.publish_year);
+            meta.publish_date = ActiveValue::Set(details.publish_date);
+            meta.free_creators = ActiveValue::Set(free_creators);
+            meta.watch_providers = ActiveValue::Set(watch_providers);
+            meta.anime_specifics = ActiveValue::Set(details.anime_specifics);
+            meta.audio_book_specifics = ActiveValue::Set(details.audio_book_specifics);
+            meta.manga_specifics = ActiveValue::Set(details.manga_specifics);
+            meta.movie_specifics = ActiveValue::Set(details.movie_specifics);
+            meta.podcast_specifics = ActiveValue::Set(details.podcast_specifics);
+            meta.show_specifics = ActiveValue::Set(details.show_specifics);
+            meta.book_specifics = ActiveValue::Set(details.book_specifics);
+            meta.video_game_specifics = ActiveValue::Set(details.video_game_specifics);
+            meta.visual_novel_specifics = ActiveValue::Set(details.visual_novel_specifics);
+            meta.external_identifiers = ActiveValue::Set(details.external_identifiers);
+            let metadata = meta.update(&ss.db).await.unwrap();
+
+            change_metadata_associations(
+                &metadata.id,
+                metadata.lot,
+                metadata.source,
+                details.genres,
+                details.suggestions,
+                details.group_identifiers,
+                details.people,
+                ss,
+            )
+            .await?;
+            notifications
+        }
+        Err(e) => {
+            ryot_log!(
+                error,
+                "Error while updating metadata = {:?}: {:?}",
+                metadata_id,
+                e
+            );
+            vec![]
+        }
+    };
+    ryot_log!(debug, "Updated metadata for {:?}", metadata_id);
+    Ok(notifications)
+}
+
+pub async fn get_entities_monitored_by(
+    entity_id: &String,
+    entity_lot: EntityLot,
+    db: &DatabaseConnection,
+) -> Result<Vec<String>> {
+    let all_entities = MonitoredEntity::find()
+        .select_only()
+        .column(monitored_entity::Column::UserId)
+        .filter(monitored_entity::Column::EntityId.eq(entity_id))
+        .filter(monitored_entity::Column::EntityLot.eq(entity_lot))
+        .into_tuple::<String>()
+        .all(db)
+        .await?;
+    Ok(all_entities)
+}
+
+pub async fn queue_notifications_to_user_platforms(
+    user_id: &String,
+    msg: &str,
+    db: &DatabaseConnection,
+) -> Result<bool> {
+    let user_details = user_by_id(db, user_id).await?;
+    if user_details.preferences.notifications.enabled {
+        let insert_data = queued_notification::ActiveModel {
+            user_id: ActiveValue::Set(user_id.to_owned()),
+            message: ActiveValue::Set(msg.to_owned()),
+            ..Default::default()
+        };
+        insert_data.insert(db).await?;
+    } else {
+        ryot_log!(debug, "User has disabled notifications");
+    }
+    Ok(true)
+}
+
+pub async fn queue_media_state_changed_notification_for_user(
+    user_id: &String,
+    notification: &(String, MediaStateChanged),
+    ss: &Arc<SupportingService>,
+) -> Result<()> {
+    let (msg, change) = notification;
+    let notification_preferences = user_preferences_by_id(user_id, ss).await?.notifications;
+    if notification_preferences.enabled && notification_preferences.to_send.contains(change) {
+        queue_notifications_to_user_platforms(user_id, msg, &ss.db)
+            .await
+            .trace_ok();
+    } else {
+        ryot_log!(
+            debug,
+            "User id = {user_id} has disabled notifications for {change}"
+        );
+    }
+    Ok(())
+}
+
+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();
+    if !notifications.is_empty() {
+        let users_to_notify =
+            get_entities_monitored_by(metadata_id, EntityLot::Metadata, &ss.db).await?;
+        for notification in notifications {
+            for user_id in users_to_notify.iter() {
+                queue_media_state_changed_notification_for_user(user_id, &notification, ss)
+                    .await
+                    .trace_ok();
+            }
+        }
+    }
+    Ok(())
+}
+
+pub async fn commit_metadata_internal(
+    details: MediaDetails,
+    is_partial: Option<bool>,
+    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),
+        description: ActiveValue::Set(details.description),
+        publish_year: ActiveValue::Set(details.publish_year),
+        publish_date: ActiveValue::Set(details.publish_date),
+        images: ActiveValue::Set(Some(images)),
+        videos: ActiveValue::Set(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),
+        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)
+}
+
+pub async fn commit_metadata(
+    input: CommitMediaInput,
+    ss: &Arc<SupportingService>,
+) -> Result<metadata::Model> {
+    if 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()))
+        .one(&ss.db)
+        .await?
+    {
+        let cached_metadata = CommitCache {
+            id: m.id.clone(),
+            lot: EntityLot::Metadata,
+        };
+        if ss.commit_cache.get(&cached_metadata).await.is_some() {
+            return Ok(m);
+        }
+        ss.commit_cache.insert(cached_metadata.clone(), ()).await;
+        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?;
+        }
+        ss.commit_cache.remove(&cached_metadata).await;
+        Ok(m)
+    } else {
+        let details = details_from_provider(input.lot, input.source, &input.identifier, ss).await?;
+        let media = commit_metadata_internal(details, None, ss).await?;
+        Ok(media)
+    }
+}
+
+pub async fn deploy_update_metadata_job(
+    metadata_id: &String,
+    force_update: bool,
+    perform_application_job: &MemoryStorage<ApplicationJob>,
+) -> Result<bool> {
+    perform_application_job
+        .clone()
+        .enqueue(ApplicationJob::UpdateMetadata(
+            metadata_id.to_owned(),
+            force_update,
+        ))
+        .await
+        .unwrap();
+    Ok(true)
+}
+
+pub async fn deploy_background_job(
+    user_id: &String,
+    job_name: BackgroundJob,
+    ss: &Arc<SupportingService>,
+) -> Result<bool> {
+    let core_storage = &mut ss.perform_core_application_job.clone();
+    let storage = &mut ss.perform_application_job.clone();
+    match job_name {
+        BackgroundJob::UpdateAllMetadata
+        | BackgroundJob::UpdateAllExercises
+        | BackgroundJob::RecalculateCalendarEvents
+        | BackgroundJob::PerformBackgroundTasks => {
+            admin_account_guard(&ss.db, user_id).await?;
+        }
+        _ => {}
+    }
+    match job_name {
+        BackgroundJob::UpdateAllMetadata => {
+            let many_metadata = Metadata::find()
+                .select_only()
+                .column(metadata::Column::Id)
+                .order_by_asc(metadata::Column::LastUpdatedOn)
+                .into_tuple::<String>()
+                .all(&ss.db)
+                .await
+                .unwrap();
+            for metadata_id in many_metadata {
+                deploy_update_metadata_job(&metadata_id, true, storage).await?;
+            }
+        }
+        BackgroundJob::UpdateAllExercises => {
+            storage
+                .enqueue(ApplicationJob::UpdateExerciseLibrary)
+                .await
+                .unwrap();
+        }
+        BackgroundJob::RecalculateCalendarEvents => {
+            storage
+                .enqueue(ApplicationJob::RecalculateCalendarEvents)
+                .await
+                .unwrap();
+        }
+        BackgroundJob::PerformBackgroundTasks => {
+            storage
+                .enqueue(ApplicationJob::PerformBackgroundTasks)
+                .await
+                .unwrap();
+        }
+        BackgroundJob::SyncIntegrationsData => {
+            core_storage
+                .enqueue(CoreApplicationJob::SyncIntegrationsData(user_id.to_owned()))
+                .await
+                .unwrap();
+        }
+        BackgroundJob::CalculateUserActivitiesAndSummary => {
+            storage
+                .enqueue(ApplicationJob::RecalculateUserActivitiesAndSummary(
+                    user_id.to_owned(),
+                    true,
+                ))
+                .await
+                .unwrap();
+        }
+        BackgroundJob::ReEvaluateUserWorkouts => {
+            storage
+                .enqueue(ApplicationJob::ReEvaluateUserWorkouts(user_id.to_owned()))
+                .await
+                .unwrap();
+        }
+    };
+    Ok(true)
+}
+
+pub async fn post_review(
+    user_id: &String,
+    input: CreateOrUpdateReviewInput,
+    ss: &Arc<SupportingService>,
+) -> Result<StringIdObject> {
+    let preferences = user_preferences_by_id(user_id, ss).await?;
+    if preferences.general.disable_reviews {
+        return Err(Error::new("Reviews are disabled"));
+    }
+    let show_ei = if let (Some(season), Some(episode)) =
+        (input.show_season_number, input.show_episode_number)
+    {
+        Some(SeenShowExtraInformation { season, episode })
+    } else {
+        None
+    };
+    let podcast_ei = input
+        .podcast_episode_number
+        .map(|episode| SeenPodcastExtraInformation { episode });
+    let anime_ei = input
+        .anime_episode_number
+        .map(|episode| SeenAnimeExtraInformation {
+            episode: Some(episode),
+        });
+    let manga_ei = if input.manga_chapter_number.is_none() && input.manga_volume_number.is_none() {
+        None
+    } else {
+        Some(SeenMangaExtraInformation {
+            chapter: input.manga_chapter_number,
+            volume: input.manga_volume_number,
+        })
+    };
+
+    if input.rating.is_none() && input.text.is_none() {
+        return Err(Error::new("At-least one of rating or review is required."));
+    }
+    let mut review_obj =
+        review::ActiveModel {
+            id: match input.review_id.clone() {
+                Some(i) => ActiveValue::Unchanged(i),
+                None => ActiveValue::NotSet,
+            },
+            rating: ActiveValue::Set(input.rating.map(
+                |r| match preferences.general.review_scale {
+                    UserReviewScale::OutOfFive => r * dec!(20),
+                    UserReviewScale::OutOfHundred | UserReviewScale::ThreePointSmiley => r,
+                },
+            )),
+            text: ActiveValue::Set(input.text),
+            user_id: ActiveValue::Set(user_id.to_owned()),
+            show_extra_information: ActiveValue::Set(show_ei),
+            anime_extra_information: ActiveValue::Set(anime_ei),
+            manga_extra_information: ActiveValue::Set(manga_ei),
+            podcast_extra_information: ActiveValue::Set(podcast_ei),
+            comments: ActiveValue::Set(vec![]),
+            ..Default::default()
+        };
+    let entity_id = input.entity_id.clone();
+    match input.entity_lot {
+        EntityLot::Metadata => review_obj.metadata_id = ActiveValue::Set(Some(entity_id)),
+        EntityLot::Person => review_obj.person_id = ActiveValue::Set(Some(entity_id)),
+        EntityLot::MetadataGroup => {
+            review_obj.metadata_group_id = ActiveValue::Set(Some(entity_id))
+        }
+        EntityLot::Collection => review_obj.collection_id = ActiveValue::Set(Some(entity_id)),
+        EntityLot::Exercise => review_obj.exercise_id = ActiveValue::Set(Some(entity_id)),
+        EntityLot::Workout | EntityLot::WorkoutTemplate => unreachable!(),
+    };
+    if let Some(s) = input.is_spoiler {
+        review_obj.is_spoiler = ActiveValue::Set(s);
+    }
+    if let Some(v) = input.visibility {
+        review_obj.visibility = ActiveValue::Set(v);
+    }
+    if let Some(d) = input.date {
+        review_obj.posted_on = ActiveValue::Set(d);
+    }
+    let insert = review_obj.save(&ss.db).await.unwrap();
+    if insert.visibility.unwrap() == Visibility::Public {
+        let entity_lot = insert.entity_lot.unwrap();
+        let id = insert.entity_id.unwrap();
+        let obj_title = match entity_lot {
+            EntityLot::Metadata => Metadata::find_by_id(&id).one(&ss.db).await?.unwrap().title,
+            EntityLot::MetadataGroup => {
+                MetadataGroup::find_by_id(&id)
+                    .one(&ss.db)
+                    .await?
+                    .unwrap()
+                    .title
+            }
+            EntityLot::Person => Person::find_by_id(&id).one(&ss.db).await?.unwrap().name,
+            EntityLot::Collection => Collection::find_by_id(&id).one(&ss.db).await?.unwrap().name,
+            EntityLot::Exercise => id.clone(),
+            EntityLot::Workout | EntityLot::WorkoutTemplate => unreachable!(),
+        };
+        let user = user_by_id(&ss.db, &insert.user_id.unwrap()).await?;
+        // DEV: Do not send notification if updating a review
+        if input.review_id.is_none() {
+            ss.perform_core_application_job
+                .clone()
+                .enqueue(CoreApplicationJob::ReviewPosted(ReviewPostedEvent {
+                    obj_title,
+                    entity_lot,
+                    obj_id: id,
+                    username: user.name,
+                    review_id: insert.id.clone().unwrap(),
+                }))
+                .await
+                .unwrap();
+        }
+    }
+    Ok(StringIdObject {
+        id: insert.id.unwrap(),
+    })
+}
+
+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) => 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(&ss.db).await?;
+            new_group.id
+        }
+    };
+    Ok((group_id, associated_items))
+}
+
+async fn seen_history(
+    user_id: &String,
+    metadata_id: &String,
+    db: &DatabaseConnection,
+) -> Result<Vec<seen::Model>> {
+    let seen_items = Seen::find()
+        .filter(seen::Column::UserId.eq(user_id))
+        .filter(seen::Column::MetadataId.eq(metadata_id))
+        .order_by_desc(seen::Column::LastUpdatedOn)
+        .all(db)
+        .await
+        .unwrap();
+    Ok(seen_items)
+}
+
+pub async fn is_metadata_finished_by_user(
+    user_id: &String,
+    metadata_id: &String,
+    db: &DatabaseConnection,
+) -> Result<(bool, Vec<seen::Model>)> {
+    let metadata = Metadata::find_by_id(metadata_id)
+        .one(db)
+        .await
+        .unwrap()
+        .unwrap();
+    let seen_history = seen_history(user_id, metadata_id, db).await?;
+    let is_finished = if metadata.lot == MediaLot::Podcast
+        || metadata.lot == MediaLot::Show
+        || metadata.lot == MediaLot::Anime
+        || metadata.lot == MediaLot::Manga
+    {
+        // DEV: If all episodes have been seen the same number of times, the media can be
+        // considered finished.
+        let all_episodes = if let Some(s) = metadata.show_specifics {
+            s.seasons
+                .into_iter()
+                .filter(|s| !SHOW_SPECIAL_SEASON_NAMES.contains(&s.name.as_str()))
+                .flat_map(|s| {
+                    s.episodes
+                        .into_iter()
+                        .map(move |e| format!("{}-{}", s.season_number, e.episode_number))
+                })
+                .collect_vec()
+        } else if let Some(p) = metadata.podcast_specifics {
+            p.episodes
+                .into_iter()
+                .map(|e| format!("{}", e.number))
+                .collect_vec()
+        } else if let Some(e) = metadata.anime_specifics.and_then(|a| a.episodes) {
+            (1..e + 1).map(|e| format!("{}", e)).collect_vec()
+        } else if let Some(c) = metadata.manga_specifics.and_then(|m| m.chapters) {
+            let one = Decimal::one();
+            (0..c.to_u32().unwrap_or(0))
+                .map(|i| Decimal::from(i) + one)
+                .map(|d| d.to_string())
+                .collect_vec()
+        } else {
+            vec![]
+        };
+        if all_episodes.is_empty() {
+            return Ok((true, seen_history));
+        }
+        let mut bag =
+            HashMap::<String, i32>::from_iter(all_episodes.iter().cloned().map(|e| (e, 0)));
+        seen_history
+            .clone()
+            .into_iter()
+            .map(|h| {
+                if let Some(s) = h.show_extra_information {
+                    format!("{}-{}", s.season, s.episode)
+                } else if let Some(p) = h.podcast_extra_information {
+                    format!("{}", p.episode)
+                } else if let Some(a) = h.anime_extra_information.and_then(|a| a.episode) {
+                    format!("{}", a)
+                } else if let Some(m) = h.manga_extra_information.and_then(|m| m.chapter) {
+                    format!("{}", m)
+                } else {
+                    String::new()
+                }
+            })
+            .for_each(|ep| {
+                bag.entry(ep).and_modify(|c| *c += 1);
+            });
+        let values = bag.values().cloned().collect_vec();
+
+        let min_value = values.iter().min();
+        let max_value = values.iter().max();
+
+        match (min_value, max_value) {
+            (Some(min), Some(max)) => min == max && *min != 0,
+            _ => false,
+        }
+    } else {
+        seen_history.iter().any(|h| h.state == SeenState::Completed)
+    };
+    Ok((is_finished, seen_history))
+}
+
+pub async fn after_media_seen_tasks(seen: seen::Model, ss: &Arc<SupportingService>) -> Result<()> {
+    let add_entity_to_collection = |collection_name: &str| {
+        add_entity_to_collection(
+            &seen.user_id,
+            ChangeCollectionToEntityInput {
+                creator_user_id: seen.user_id.clone(),
+                collection_name: collection_name.to_string(),
+                entity_id: seen.metadata_id.clone(),
+                entity_lot: EntityLot::Metadata,
+                ..Default::default()
+            },
+            ss,
+        )
+    };
+    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(),
+                collection_name: collection_name.to_string(),
+                entity_id: seen.metadata_id.clone(),
+                entity_lot: EntityLot::Metadata,
+                ..Default::default()
+            },
+        )
+    };
+    remove_entity_from_collection(&DefaultCollection::Watchlist.to_string())
+        .await
+        .ok();
+    match seen.state {
+        SeenState::InProgress => {
+            for col in &[DefaultCollection::InProgress, DefaultCollection::Monitoring] {
+                add_entity_to_collection(&col.to_string()).await.ok();
+            }
+        }
+        SeenState::Dropped | SeenState::OnAHold => {
+            remove_entity_from_collection(&DefaultCollection::InProgress.to_string())
+                .await
+                .ok();
+        }
+        SeenState::Completed => {
+            let metadata = Metadata::find_by_id(&seen.metadata_id)
+                .one(&ss.db)
+                .await?
+                .unwrap();
+            if metadata.lot == MediaLot::Podcast
+                || metadata.lot == MediaLot::Show
+                || metadata.lot == MediaLot::Anime
+                || metadata.lot == MediaLot::Manga
+            {
+                let (is_complete, _) =
+                    is_metadata_finished_by_user(&seen.user_id, &seen.metadata_id, &ss.db).await?;
+                if is_complete {
+                    remove_entity_from_collection(&DefaultCollection::InProgress.to_string())
+                        .await
+                        .ok();
+                    add_entity_to_collection(&DefaultCollection::Completed.to_string())
+                        .await
+                        .ok();
+                } else {
+                    for col in &[DefaultCollection::InProgress, DefaultCollection::Monitoring] {
+                        add_entity_to_collection(&col.to_string()).await.ok();
+                    }
+                }
+            } else {
+                add_entity_to_collection(&DefaultCollection::Completed.to_string())
+                    .await
+                    .ok();
+                for col in &[DefaultCollection::InProgress, DefaultCollection::Monitoring] {
+                    remove_entity_from_collection(&col.to_string()).await.ok();
+                }
+            };
+        }
+    };
+    Ok(())
+}
+
+pub async fn progress_update(
+    input: ProgressUpdateInput,
+    user_id: &String,
+    // update only if media has not been consumed for this user in the last `n` duration
+    respect_cache: bool,
+    ss: &Arc<SupportingService>,
+) -> Result<ProgressUpdateResultUnion> {
+    let cache = ApplicationCacheKey::ProgressUpdateCache {
+        user_id: user_id.to_owned(),
+        metadata_id: input.metadata_id.clone(),
+        show_season_number: input.show_season_number,
+        show_episode_number: input.show_episode_number,
+        manga_volume_number: input.manga_volume_number,
+        manga_chapter_number: input.manga_chapter_number,
+        anime_episode_number: input.anime_episode_number,
+        podcast_episode_number: input.podcast_episode_number,
+    };
+    let in_cache = ss.cache_service.get(cache.clone()).await?;
+    if respect_cache && in_cache.is_some() {
+        ryot_log!(debug, "Seen is already in cache");
+        return Ok(ProgressUpdateResultUnion::Error(ProgressUpdateError {
+            error: ProgressUpdateErrorVariant::AlreadySeen,
+        }));
+    }
+    ryot_log!(debug, "Input for progress_update = {:?}", input);
+
+    let all_prev_seen = Seen::find()
+        .filter(seen::Column::Progress.lt(100))
+        .filter(seen::Column::UserId.eq(user_id))
+        .filter(seen::Column::State.ne(SeenState::Dropped))
+        .filter(seen::Column::MetadataId.eq(&input.metadata_id))
+        .order_by_desc(seen::Column::LastUpdatedOn)
+        .all(&ss.db)
+        .await
+        .unwrap();
+    #[derive(Debug, Serialize, Deserialize, Enum, Clone, PartialEq, Eq, Copy)]
+    enum ProgressUpdateAction {
+        Update,
+        Now,
+        InThePast,
+        JustStarted,
+        ChangeState,
+    }
+    let action = match input.change_state {
+        None => match input.progress {
+            None => ProgressUpdateAction::ChangeState,
+            Some(p) => {
+                if p == dec!(100) {
+                    match input.date {
+                        None => ProgressUpdateAction::InThePast,
+                        Some(u) => {
+                            if get_current_date(&ss.timezone) == u {
+                                if all_prev_seen.is_empty() {
+                                    ProgressUpdateAction::Now
+                                } else {
+                                    ProgressUpdateAction::Update
+                                }
+                            } else {
+                                ProgressUpdateAction::InThePast
+                            }
+                        }
+                    }
+                } else if all_prev_seen.is_empty() {
+                    ProgressUpdateAction::JustStarted
+                } else {
+                    ProgressUpdateAction::Update
+                }
+            }
+        },
+        Some(_) => ProgressUpdateAction::ChangeState,
+    };
+    ryot_log!(debug, "Progress update action = {:?}", action);
+    let err = || {
+        Ok(ProgressUpdateResultUnion::Error(ProgressUpdateError {
+            error: ProgressUpdateErrorVariant::NoSeenInProgress,
+        }))
+    };
+    let seen = match action {
+        ProgressUpdateAction::Update => {
+            let prev_seen = all_prev_seen[0].clone();
+            let progress = input.progress.unwrap();
+            let watched_on = prev_seen.provider_watched_on.clone();
+            if prev_seen.progress == progress && watched_on == input.provider_watched_on {
+                ryot_log!(debug, "No progress update required");
+                return Ok(ProgressUpdateResultUnion::Error(ProgressUpdateError {
+                    error: ProgressUpdateErrorVariant::UpdateWithoutProgressUpdate,
+                }));
+            }
+            let mut updated_at = prev_seen.updated_at.clone();
+            let now = Utc::now();
+            if prev_seen.progress != progress {
+                updated_at.push(now);
+            }
+            let mut last_seen: seen::ActiveModel = prev_seen.into();
+            last_seen.state = ActiveValue::Set(SeenState::InProgress);
+            last_seen.progress = ActiveValue::Set(progress);
+            last_seen.updated_at = ActiveValue::Set(updated_at);
+            last_seen.provider_watched_on =
+                ActiveValue::Set(input.provider_watched_on.or(watched_on));
+            if progress == dec!(100) {
+                last_seen.finished_on = ActiveValue::Set(Some(now.date_naive()));
+            }
+
+            // This is needed for manga as some of the apps will update in weird orders
+            // For example with komga mihon will update out of order to the server
+            if input.manga_chapter_number.is_some() {
+                last_seen.manga_extra_information =
+                    ActiveValue::set(Some(SeenMangaExtraInformation {
+                        chapter: input.manga_chapter_number,
+                        volume: input.manga_volume_number,
+                    }))
+            }
+
+            last_seen.update(&ss.db).await.unwrap()
+        }
+        ProgressUpdateAction::ChangeState => {
+            let new_state = input.change_state.unwrap_or(SeenState::Dropped);
+            let last_seen = Seen::find()
+                .filter(seen::Column::UserId.eq(user_id))
+                .filter(seen::Column::MetadataId.eq(input.metadata_id))
+                .order_by_desc(seen::Column::LastUpdatedOn)
+                .one(&ss.db)
+                .await
+                .unwrap();
+            match last_seen {
+                Some(ls) => {
+                    let watched_on = ls.provider_watched_on.clone();
+                    let mut updated_at = ls.updated_at.clone();
+                    let now = Utc::now();
+                    updated_at.push(now);
+                    let mut last_seen: seen::ActiveModel = ls.into();
+                    last_seen.state = ActiveValue::Set(new_state);
+                    last_seen.updated_at = ActiveValue::Set(updated_at);
+                    last_seen.provider_watched_on =
+                        ActiveValue::Set(input.provider_watched_on.or(watched_on));
+                    last_seen.update(&ss.db).await.unwrap()
+                }
+                None => {
+                    return err();
+                }
+            }
+        }
+        ProgressUpdateAction::Now
+        | ProgressUpdateAction::InThePast
+        | ProgressUpdateAction::JustStarted => {
+            let meta = Metadata::find_by_id(&input.metadata_id)
+                .one(&ss.db)
+                .await
+                .unwrap()
+                .unwrap();
+            ryot_log!(
+                debug,
+                "Progress update for meta {:?} ({:?})",
+                meta.title,
+                meta.lot
+            );
+
+            let show_ei = if matches!(meta.lot, MediaLot::Show) {
+                let season = input.show_season_number.ok_or_else(|| {
+                    Error::new("Season number is required for show progress update")
+                })?;
+                let episode = input.show_episode_number.ok_or_else(|| {
+                    Error::new("Episode number is required for show progress update")
+                })?;
+                Some(SeenShowExtraInformation { season, episode })
+            } else {
+                None
+            };
+            let podcast_ei = if matches!(meta.lot, MediaLot::Podcast) {
+                let episode = input.podcast_episode_number.ok_or_else(|| {
+                    Error::new("Episode number is required for podcast progress update")
+                })?;
+                Some(SeenPodcastExtraInformation { episode })
+            } else {
+                None
+            };
+            let anime_ei = if matches!(meta.lot, MediaLot::Anime) {
+                Some(SeenAnimeExtraInformation {
+                    episode: input.anime_episode_number,
+                })
+            } else {
+                None
+            };
+            let manga_ei = if matches!(meta.lot, MediaLot::Manga) {
+                Some(SeenMangaExtraInformation {
+                    chapter: input.manga_chapter_number,
+                    volume: input.manga_volume_number,
+                })
+            } else {
+                None
+            };
+            let finished_on = if action == ProgressUpdateAction::JustStarted {
+                None
+            } else {
+                input.date
+            };
+            ryot_log!(debug, "Progress update finished on = {:?}", finished_on);
+            let (progress, started_on) = if matches!(action, ProgressUpdateAction::JustStarted) {
+                (
+                    input.progress.unwrap_or(dec!(0)),
+                    Some(Utc::now().date_naive()),
+                )
+            } else {
+                (dec!(100), None)
+            };
+            ryot_log!(debug, "Progress update percentage = {:?}", progress);
+            let seen_insert = seen::ActiveModel {
+                progress: ActiveValue::Set(progress),
+                user_id: ActiveValue::Set(user_id.to_owned()),
+                metadata_id: ActiveValue::Set(input.metadata_id),
+                started_on: ActiveValue::Set(started_on),
+                finished_on: ActiveValue::Set(finished_on),
+                state: ActiveValue::Set(SeenState::InProgress),
+                provider_watched_on: ActiveValue::Set(input.provider_watched_on),
+                show_extra_information: ActiveValue::Set(show_ei),
+                podcast_extra_information: ActiveValue::Set(podcast_ei),
+                anime_extra_information: ActiveValue::Set(anime_ei),
+                manga_extra_information: ActiveValue::Set(manga_ei),
+                ..Default::default()
+            };
+            seen_insert.insert(&ss.db).await.unwrap()
+        }
+    };
+    ryot_log!(debug, "Progress update = {:?}", seen);
+    let id = seen.id.clone();
+    if seen.state == SeenState::Completed && respect_cache {
+        ss.cache_service
+            .set_with_expiry(cache, ss.config.server.progress_update_threshold)
+            .await?;
+    }
+    after_media_seen_tasks(seen, ss).await?;
+    Ok(ProgressUpdateResultUnion::Ok(StringIdObject { id }))
+}
+
+fn convert_review_into_input(
+    review: &ImportOrExportItemRating,
+    preferences: &UserPreferences,
+    entity_id: String,
+    entity_lot: EntityLot,
+) -> Option<CreateOrUpdateReviewInput> {
+    if review.review.is_none() && review.rating.is_none() {
+        ryot_log!(debug, "Skipping review since it has no content");
+        return None;
+    }
+    let rating = match preferences.general.review_scale {
+        UserReviewScale::OutOfFive => review.rating.map(|rating| rating / dec!(20)),
+        UserReviewScale::OutOfHundred | UserReviewScale::ThreePointSmiley => review.rating,
+    };
+    let text = review.review.clone().and_then(|r| r.text);
+    let is_spoiler = review.review.clone().map(|r| r.spoiler.unwrap_or(false));
+    let date = review.review.clone().map(|r| r.date);
+    Some(CreateOrUpdateReviewInput {
+        rating,
+        text,
+        is_spoiler,
+        visibility: review.review.clone().and_then(|r| r.visibility),
+        date: date.flatten(),
+        entity_id,
+        entity_lot,
+        show_season_number: review.show_season_number,
+        show_episode_number: review.show_episode_number,
+        podcast_episode_number: review.podcast_episode_number,
+        manga_chapter_number: review.manga_chapter_number,
+        ..Default::default()
+    })
+}
+
+pub async fn create_user_measurement(
+    user_id: &String,
+    mut input: user_measurement::Model,
+    db: &DatabaseConnection,
+) -> Result<DateTimeUtc> {
+    input.user_id = user_id.to_owned();
+    let um: user_measurement::ActiveModel = input.into();
+    let um = um.insert(db).await?;
+    Ok(um.timestamp)
+}
+
+fn get_best_set_index(records: &[WorkoutSetRecord]) -> Option<usize> {
+    records
+        .iter()
+        .enumerate()
+        .max_by_key(|(_, record)| {
+            record.statistic.duration.unwrap_or(dec!(0))
+                + record.statistic.distance.unwrap_or(dec!(0))
+                + record.statistic.reps.unwrap_or(dec!(0))
+                + record.statistic.weight.unwrap_or(dec!(0))
+        })
+        .map(|(index, _)| index)
+}
+
+fn get_index_of_highest_pb(
+    records: &[WorkoutSetRecord],
+    pb_type: &WorkoutSetPersonalBest,
+) -> Option<usize> {
+    let record = records.iter().reduce(|record1, record2| {
+        let pb1 = record1.get_personal_best(pb_type);
+        let pb2 = record2.get_personal_best(pb_type);
+        match (pb1, pb2) {
+            (Some(pb1), Some(pb2)) => {
+                if pb1 > pb2 {
+                    record1
+                } else {
+                    record2
+                }
+            }
+            _ => record1,
+        }
+    });
+    record.and_then(|r| records.iter().position(|l| l == r))
+}
+
+/// Create a workout in the database and also update user and exercise associations.
+pub async fn create_or_update_workout(
+    input: UserWorkoutInput,
+    user_id: &String,
+    ss: &Arc<SupportingService>,
+) -> AnyhowResult<String> {
+    let end_time = input.end_time;
+    let mut input = input;
+    let (new_workout_id, to_update_workout) = match &input.update_workout_id {
+        Some(id) => (
+            id.to_owned(),
+            // DEV: Unwrap to make sure we error out early if the workout to edit does not exist
+            Some(Workout::find_by_id(id).one(&ss.db).await?.unwrap()),
+        ),
+        None => (
+            input
+                .create_workout_id
+                .unwrap_or_else(|| format!("wor_{}", nanoid!(12))),
+            None,
+        ),
+    };
+    ryot_log!(debug, "Creating new workout with id = {}", new_workout_id);
+    let mut exercises = vec![];
+    let mut workout_totals = vec![];
+    if input.exercises.is_empty() {
+        bail!("This workout has no associated exercises")
+    }
+    let mut first_set_of_exercise_confirmed_at = input
+        .exercises
+        .first()
+        .unwrap()
+        .sets
+        .first()
+        .unwrap()
+        .confirmed_at;
+    for (exercise_idx, ex) in input.exercises.iter_mut().enumerate() {
+        if ex.sets.is_empty() {
+            bail!("This exercise has no associated sets")
+        }
+        let db_ex = match Exercise::find_by_id(ex.exercise_id.clone())
+            .one(&ss.db)
+            .await?
+        {
+            None => {
+                ryot_log!(error, "Exercise with id = {} not found", ex.exercise_id);
+                continue;
+            }
+            Some(e) => e,
+        };
+        let mut sets = vec![];
+        let mut total = WorkoutOrExerciseTotals::default();
+        let association = UserToEntity::find()
+            .filter(user_to_entity::Column::UserId.eq(user_id))
+            .filter(user_to_entity::Column::ExerciseId.eq(ex.exercise_id.clone()))
+            .one(&ss.db)
+            .await
+            .ok()
+            .flatten();
+        let history_item = UserToExerciseHistoryExtraInformation {
+            best_set: None,
+            idx: exercise_idx,
+            workout_id: new_workout_id.clone(),
+            workout_end_on: end_time,
+        };
+        let association = match association {
+            None => {
+                let user_to_ex = user_to_entity::ActiveModel {
+                    user_id: ActiveValue::Set(user_id.clone()),
+                    exercise_id: ActiveValue::Set(Some(ex.exercise_id.clone())),
+                    exercise_extra_information: ActiveValue::Set(Some(
+                        UserToExerciseExtraInformation {
+                            history: vec![history_item],
+                            lifetime_stats: WorkoutOrExerciseTotals::default(),
+                            personal_bests: vec![],
+                        },
+                    )),
+                    created_on: ActiveValue::Set(
+                        first_set_of_exercise_confirmed_at.unwrap_or(end_time),
+                    ),
+                    exercise_num_times_interacted: ActiveValue::Set(Some(1)),
+                    ..Default::default()
+                };
+                user_to_ex.insert(&ss.db).await.unwrap()
+            }
+            Some(e) => {
+                let last_updated_on = e.last_updated_on;
+                let mut extra_info = e.exercise_extra_information.clone().unwrap_or_default();
+                extra_info.history.insert(0, history_item);
+                let mut to_update: user_to_entity::ActiveModel = e.into();
+                to_update.exercise_num_times_interacted =
+                    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));
+                to_update.update(&ss.db).await?
+            }
+        };
+        if let Some(d) = ex.rest_time {
+            total.rest_time += d * (ex.sets.len() - 1) as u16;
+        }
+        ex.sets
+            .sort_unstable_by_key(|s| s.confirmed_at.unwrap_or_default());
+        for set in ex.sets.iter_mut() {
+            let mut actual_rest_time = None;
+            if exercise_idx != 0
+                && set.confirmed_at.is_some()
+                && first_set_of_exercise_confirmed_at.is_some()
+            {
+                actual_rest_time = Some(
+                    (set.confirmed_at.unwrap() - first_set_of_exercise_confirmed_at.unwrap())
+                        .num_seconds(),
+                );
+            }
+            first_set_of_exercise_confirmed_at = set.confirmed_at;
+            set.remove_invalids(&db_ex.lot);
+            if let Some(r) = set.statistic.reps {
+                total.reps += r;
+                if let Some(w) = set.statistic.weight {
+                    total.weight += w * r;
+                }
+            }
+            if let Some(d) = set.statistic.duration {
+                total.duration += d;
+            }
+            if let Some(d) = set.statistic.distance {
+                total.distance += d;
+            }
+            let mut totals = WorkoutSetTotals::default();
+            if let (Some(we), Some(re)) = (&set.statistic.weight, &set.statistic.reps) {
+                totals.weight = Some(we * re);
+            }
+            let mut value = WorkoutSetRecord {
+                lot: set.lot,
+                actual_rest_time,
+                totals: Some(totals),
+                note: set.note.clone(),
+                personal_bests: Some(vec![]),
+                confirmed_at: set.confirmed_at,
+                statistic: set.statistic.clone(),
+            };
+            value.statistic.one_rm = value.calculate_one_rm();
+            value.statistic.pace = value.calculate_pace();
+            value.statistic.volume = value.calculate_volume();
+            sets.push(value);
+        }
+        let mut personal_bests = association
+            .exercise_extra_information
+            .clone()
+            .unwrap_or_default()
+            .personal_bests;
+        let types_of_prs = LOT_MAPPINGS
+            .iter()
+            .find(|lm| lm.0 == db_ex.lot)
+            .map(|lm| lm.1)
+            .unwrap();
+        for best_type in types_of_prs.iter() {
+            let set_idx = get_index_of_highest_pb(&sets, best_type).unwrap();
+            let possible_record = personal_bests
+                .iter()
+                .find(|pb| pb.lot == *best_type)
+                .and_then(|record| record.sets.first());
+            let set = sets.get_mut(set_idx).unwrap();
+            if let Some(r) = possible_record {
+                if let Some(workout) = Workout::find_by_id(r.workout_id.clone())
+                    .one(&ss.db)
+                    .await?
+                {
+                    let workout_set =
+                        workout.information.exercises[r.exercise_idx].sets[r.set_idx].clone();
+                    if set.get_personal_best(best_type) > workout_set.get_personal_best(best_type) {
+                        if let Some(ref mut set_personal_bests) = set.personal_bests {
+                            set_personal_bests.push(*best_type);
+                        }
+                        total.personal_bests_achieved += 1;
+                    }
+                }
+            } else {
+                if let Some(ref mut set_personal_bests) = set.personal_bests {
+                    set_personal_bests.push(*best_type);
+                }
+                total.personal_bests_achieved += 1;
+            }
+        }
+        workout_totals.push(total.clone());
+        for (set_idx, set) in sets.iter().enumerate() {
+            if let Some(set_personal_bests) = &set.personal_bests {
+                for best in set_personal_bests.iter() {
+                    let to_insert_record = ExerciseBestSetRecord {
+                        workout_id: new_workout_id.clone(),
+                        exercise_idx,
+                        set_idx,
+                    };
+                    if let Some(record) = personal_bests.iter_mut().find(|pb| pb.lot == *best) {
+                        let mut data = record.sets.clone();
+                        data.insert(0, to_insert_record);
+                        record.sets = data;
+                    } else {
+                        personal_bests.push(UserToExerciseBestSetExtraInformation {
+                            lot: *best,
+                            sets: vec![to_insert_record],
+                        });
+                    }
+                }
+            }
+        }
+        let best_set = get_best_set_index(&sets).and_then(|i| sets.get(i).cloned());
+        let mut association_extra_information = association
+            .exercise_extra_information
+            .clone()
+            .unwrap_or_default();
+        association_extra_information.history[0].best_set = best_set.clone();
+        let mut association: user_to_entity::ActiveModel = association.into();
+        association_extra_information.lifetime_stats += total.clone();
+        association_extra_information.personal_bests = personal_bests;
+        association.exercise_extra_information =
+            ActiveValue::Set(Some(association_extra_information));
+        association.update(&ss.db).await?;
+        exercises.push((
+            best_set,
+            db_ex.lot,
+            ProcessedExercise {
+                sets,
+                lot: db_ex.lot,
+                name: db_ex.id,
+                total: Some(total),
+                notes: ex.notes.clone(),
+                rest_time: ex.rest_time,
+                assets: ex.assets.clone(),
+                superset_with: ex.superset_with.clone(),
+            },
+        ));
+    }
+    let summary_total = workout_totals.into_iter().sum();
+    let model = workout::Model {
+        end_time,
+        name: input.name,
+        user_id: user_id.clone(),
+        id: new_workout_id.clone(),
+        start_time: input.start_time,
+        repeated_from: input.repeated_from,
+        summary: WorkoutSummary {
+            total: Some(summary_total),
+            exercises: exercises
+                .clone()
+                .into_iter()
+                .map(|(best_set, lot, e)| WorkoutSummaryExercise {
+                    best_set,
+                    lot: Some(lot),
+                    id: e.name.clone(),
+                    num_sets: e.sets.len(),
+                })
+                .collect(),
+        },
+        information: WorkoutInformation {
+            comment: input.comment,
+            assets: input.assets,
+            exercises: exercises.into_iter().map(|(_, _, ex)| ex).collect(),
+        },
+        template_id: input.template_id,
+        duration: 0,
+    };
+    let mut insert: workout::ActiveModel = model.into();
+    insert.duration = ActiveValue::NotSet;
+    if let Some(old_workout) = to_update_workout.clone() {
+        insert.end_time = ActiveValue::Set(old_workout.end_time);
+        insert.start_time = ActiveValue::Set(old_workout.start_time);
+        insert.repeated_from = ActiveValue::Set(old_workout.repeated_from.clone());
+        old_workout.delete(&ss.db).await?;
+    }
+    let data = insert.insert(&ss.db).await?;
+    if to_update_workout.is_some() {
+        deploy_job_to_re_evaluate_user_workouts(user_id, &ss.perform_application_job).await;
+    }
+    Ok(data.id)
+}
+
+async fn create_collection_and_add_entity_to_it(
+    ss: &Arc<SupportingService>,
+    user_id: &String,
+    collection_name: String,
+    entity_id: String,
+    entity_lot: EntityLot,
+) -> Result<()> {
+    create_or_update_collection(
+        &ss.db,
+        user_id,
+        CreateOrUpdateCollectionInput {
+            name: collection_name.clone(),
+            ..Default::default()
+        },
+    )
+    .await?;
+    add_entity_to_collection(
+        user_id,
+        ChangeCollectionToEntityInput {
+            entity_id,
+            entity_lot,
+            creator_user_id: user_id.clone(),
+            collection_name: collection_name.to_string(),
+            ..Default::default()
+        },
+        ss,
+    )
+    .await
+    .ok();
+    Ok(())
+}
+
+pub async fn process_import(
+    user_id: &String,
+    import: ImportResult,
+    ss: &Arc<SupportingService>,
+) -> Result<ImportResultResponse> {
+    let mut import = import;
+    let preferences = user_by_id(&ss.db, user_id).await?.preferences;
+    for m in import.metadata.iter_mut() {
+        m.seen_history.sort_by(|a, b| {
+            a.ended_on
+                .unwrap_or_default()
+                .cmp(&b.ended_on.unwrap_or_default())
+        });
+    }
+
+    let total = import.collections.len()
+        + import.metadata.len()
+        + import.metadata_groups.len()
+        + import.people.len()
+        + import.workouts.len()
+        + import.measurements.len();
+
+    for (idx, col_details) in import.collections.into_iter().enumerate() {
+        create_or_update_collection(&ss.db, user_id, col_details).await?;
+        ryot_log!(debug, "Collection {} created", idx);
+    }
+
+    for (idx, item) in import.metadata.into_iter().enumerate() {
+        let source_id = if item.source_id.is_empty() {
+            item.identifier.clone()
+        } else {
+            item.source_id.clone()
+        };
+        ryot_log!(debug, "Importing media with identifier = {:#?}", source_id);
+        let rev_length = item.reviews.len();
+        let identifier = item.identifier.clone();
+        let data = commit_metadata(
+            CommitMediaInput {
+                identifier,
+                lot: item.lot,
+                source: item.source,
+                force_update: None,
+            },
+            ss,
+        )
+        .await;
+        let metadata = match data {
+            Ok(r) => r,
+            Err(e) => {
+                ryot_log!(error, "{e:?}");
+                import.failed_items.push(ImportFailedItem {
+                    lot: Some(item.lot),
+                    step: ImportFailStep::MediaDetailsFromProvider,
+                    identifier: item.source_id.to_owned(),
+                    error: Some(e.message),
+                });
+                continue;
+            }
+        };
+        for seen in item.seen_history.iter() {
+            let progress = if seen.progress.is_some() {
+                seen.progress
+            } else {
+                Some(dec!(100))
+            };
+            if let Err(e) = progress_update(
+                ProgressUpdateInput {
+                    metadata_id: metadata.id.clone(),
+                    progress,
+                    date: seen.ended_on,
+                    show_season_number: seen.show_season_number,
+                    show_episode_number: seen.show_episode_number,
+                    podcast_episode_number: seen.podcast_episode_number,
+                    anime_episode_number: seen.anime_episode_number,
+                    manga_chapter_number: seen.manga_chapter_number,
+                    manga_volume_number: seen.manga_volume_number,
+                    provider_watched_on: seen.provider_watched_on.clone(),
+                    change_state: None,
+                },
+                user_id,
+                false,
+                ss,
+            )
+            .await
+            {
+                import.failed_items.push(ImportFailedItem {
+                    lot: Some(item.lot),
+                    step: ImportFailStep::SeenHistoryConversion,
+                    identifier: item.source_id.to_owned(),
+                    error: Some(e.message),
+                });
+            };
+        }
+        for review in item.reviews.iter() {
+            if let Some(input) = convert_review_into_input(
+                review,
+                &preferences,
+                metadata.id.clone(),
+                EntityLot::Metadata,
+            ) {
+                if let Err(e) = post_review(user_id, input, ss).await {
+                    import.failed_items.push(ImportFailedItem {
+                        lot: Some(item.lot),
+                        step: ImportFailStep::ReviewConversion,
+                        identifier: item.source_id.to_owned(),
+                        error: Some(e.message),
+                    });
+                };
+            }
+        }
+        for col in item.collections.into_iter() {
+            create_collection_and_add_entity_to_it(
+                ss,
+                user_id,
+                col,
+                metadata.id.clone(),
+                EntityLot::Metadata,
+            )
+            .await?;
+        }
+        ryot_log!(
+            debug,
+            "Imported item: {idx}, lot: {lot}, history count: {hist}, review count: {rev}",
+            idx = idx + 1,
+            lot = item.lot,
+            hist = item.seen_history.len(),
+            rev = rev_length,
+        );
+    }
+    for (idx, item) in import.metadata_groups.into_iter().enumerate() {
+        ryot_log!(
+            debug,
+            "Importing media group with identifier = {iden}",
+            iden = &item.title
+        );
+        let rev_length = item.reviews.len();
+        let data =
+            commit_metadata_group_internal(&item.identifier, item.lot, item.source, ss).await;
+        let metadata_group_id = match data {
+            Ok(r) => r.0,
+            Err(e) => {
+                ryot_log!(error, "{e:?}");
+                import.failed_items.push(ImportFailedItem {
+                    lot: Some(item.lot),
+                    step: ImportFailStep::MediaDetailsFromProvider,
+                    identifier: item.title.to_owned(),
+                    error: Some(e.message),
+                });
+                continue;
+            }
+        };
+        for review in item.reviews.iter() {
+            if let Some(input) = convert_review_into_input(
+                review,
+                &preferences,
+                metadata_group_id.clone(),
+                EntityLot::MetadataGroup,
+            ) {
+                if let Err(e) = post_review(user_id, input, ss).await {
+                    import.failed_items.push(ImportFailedItem {
+                        lot: Some(item.lot),
+                        step: ImportFailStep::ReviewConversion,
+                        identifier: item.title.to_owned(),
+                        error: Some(e.message),
+                    });
+                };
+            }
+        }
+        for col in item.collections.into_iter() {
+            create_collection_and_add_entity_to_it(
+                ss,
+                user_id,
+                col,
+                metadata_group_id.clone(),
+                EntityLot::MetadataGroup,
+            )
+            .await?;
+        }
+        ryot_log!(
+            debug,
+            "Imported item: {idx}, lot: {lot}, review count: {rev}",
+            idx = idx + 1,
+            lot = item.lot,
+            rev = rev_length,
+        );
+    }
+    for (idx, item) in import.people.into_iter().enumerate() {
+        let person = commit_person(
+            CommitPersonInput {
+                identifier: item.identifier.clone(),
+                name: item.name.clone(),
+                source: item.source,
+                source_specifics: item.source_specifics.clone(),
+            },
+            &ss.db,
+        )
+        .await?;
+        for review in item.reviews.iter() {
+            if let Some(input) = convert_review_into_input(
+                review,
+                &preferences,
+                person.id.clone(),
+                EntityLot::Person,
+            ) {
+                if let Err(e) = post_review(user_id, input, ss).await {
+                    import.failed_items.push(ImportFailedItem {
+                        lot: None,
+                        step: ImportFailStep::ReviewConversion,
+                        identifier: item.name.to_owned(),
+                        error: Some(e.message),
+                    });
+                };
+            }
+        }
+        for col in item.collections.into_iter() {
+            create_collection_and_add_entity_to_it(
+                ss,
+                user_id,
+                col,
+                person.id.clone(),
+                EntityLot::Person,
+            )
+            .await?;
+        }
+        ryot_log!(
+            debug,
+            "Imported person: {idx}, name: {name}",
+            idx = idx + 1,
+            name = item.name,
+        );
+    }
+    for (idx, workout) in import.workouts.into_iter().enumerate() {
+        if let Err(err) = create_or_update_workout(workout, user_id, ss).await {
+            import.failed_items.push(ImportFailedItem {
+                lot: None,
+                step: ImportFailStep::InputTransformation,
+                identifier: "Exercise".to_string(),
+                error: Some(err.to_string()),
+            });
+        }
+        ryot_log!(debug, "Workout {} created", idx);
+    }
+    for (idx, workout) in import.application_workouts.into_iter().enumerate() {
+        let workout_input = db_workout_to_workout_input(workout.details);
+        match create_or_update_workout(workout_input, user_id, ss).await {
+            Err(err) => {
+                import.failed_items.push(ImportFailedItem {
+                    lot: None,
+                    step: ImportFailStep::InputTransformation,
+                    identifier: "Exercise".to_string(),
+                    error: Some(err.to_string()),
+                });
+            }
+            Ok(workout_id) => {
+                for col in workout.collections.into_iter() {
+                    create_collection_and_add_entity_to_it(
+                        ss,
+                        user_id,
+                        col,
+                        workout_id.clone(),
+                        EntityLot::Workout,
+                    )
+                    .await?;
+                }
+            }
+        }
+        ryot_log!(debug, "Workout {} created", idx);
+    }
+    for (idx, measurement) in import.measurements.into_iter().enumerate() {
+        if let Err(err) = create_user_measurement(user_id, measurement, &ss.db).await {
+            import.failed_items.push(ImportFailedItem {
+                lot: None,
+                step: ImportFailStep::InputTransformation,
+                identifier: "Measurement".to_string(),
+                error: Some(err.message),
+            });
+        }
+        ryot_log!(debug, "Measurement {} created", idx);
+    }
+
+    // TODO: Allow importing exercises
+
+    let details = ImportResultResponse {
+        import: ImportDetails { total },
+        failed_items: import.failed_items,
+    };
+
+    Ok(details)
+}
+
+pub fn db_workout_to_workout_input(user_workout: workout::Model) -> UserWorkoutInput {
+    UserWorkoutInput {
+        name: user_workout.name,
+        create_workout_id: Some(user_workout.id),
+        update_workout_id: None,
+        default_rest_timer: None,
+        end_time: user_workout.end_time,
+        update_workout_template_id: None,
+        start_time: user_workout.start_time,
+        template_id: user_workout.template_id,
+        assets: user_workout.information.assets,
+        repeated_from: user_workout.repeated_from,
+        comment: user_workout.information.comment,
+        exercises: user_workout
+            .information
+            .exercises
+            .into_iter()
+            .map(|e| UserExerciseInput {
+                exercise_id: e.name,
+                sets: e
+                    .sets
+                    .into_iter()
+                    .map(|s| UserWorkoutSetRecord {
+                        lot: s.lot,
+                        note: s.note,
+                        statistic: s.statistic,
+                        confirmed_at: s.confirmed_at,
+                    })
+                    .collect(),
+                notes: e.notes,
+                rest_time: e.rest_time,
+                assets: e.assets,
+                superset_with: e.superset_with,
+            })
+            .collect(),
+    }
+}
diff --git a/docs/content/integrations.md b/docs/content/integrations.md
index 964d3b7877..7082258728 100644
--- a/docs/content/integrations.md
+++ b/docs/content/integrations.md
@@ -157,3 +157,12 @@ TMDb ID attached to their metadata.
    the zipped addon to your Kodi instance. Once installed, it will be visible under
    the "Services" sub category named "Ryot".
 4. Click on "Configure" to fill in the correct details.
+
+### Generic Json
+
+The "Generic Json" can be used to import all possible data using a generic JSON data
+format. The format of the JSON file should be `CompleteExport` as described in the
+[exporting](guides/exporting.md#type-definition) documentation.
+
+You can use this to build integrations with other services that Ryot does not support
+natively.
diff --git a/docs/includes/export-schema.ts b/docs/includes/export-schema.ts
index 0f3f1f2127..5f283ce347 100644
--- a/docs/includes/export-schema.ts
+++ b/docs/includes/export-schema.ts
@@ -353,7 +353,7 @@ export interface CompleteExport {
 	/** Data about user's media. */
 	media: ImportOrExportMediaItem[] | null;
 	/** Data about user's media groups. */
-	media_group: ImportOrExportMediaGroupItem[] | null;
+	media_groups: ImportOrExportMediaGroupItem[] | null;
 	/** Data about user's people. */
 	people: ImportOrExportPersonItem[] | null;
 	/** Data about user's workout templates. */
diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts
index aebc213734..ab485c518e 100644
--- a/libs/generated/src/graphql/backend/graphql.ts
+++ b/libs/generated/src/graphql/backend/graphql.ts
@@ -833,6 +833,7 @@ export enum IntegrationLot {
 export enum IntegrationProvider {
   Audiobookshelf = 'AUDIOBOOKSHELF',
   Emby = 'EMBY',
+  GenericJson = 'GENERIC_JSON',
   Jellyfin = 'JELLYFIN',
   Kodi = 'KODI',
   Komga = 'KOMGA',
@@ -2479,7 +2480,9 @@ export enum UserUnitSystem {
 }
 
 export type UserUpcomingCalendarEventInput = {
+  /** The number of days to select */
   nextDays?: InputMaybe<Scalars['Int']['input']>;
+  /** The number of media to select */
   nextMedia?: InputMaybe<Scalars['Int']['input']>;
 };
 


Test patch: