Compare commits

...

934 Commits

Author SHA1 Message Date
IRHM afb1fa6dfd v1.36.0 2024-03-18 02:30:39 +00:00
IRHM a58351c539 1.36.0-dev1 2024-03-18 01:41:51 +00:00
IRHM d643d349ac Plex Auth: Remove plexUserHasAccessToPlexHost and use getPlexHomeServerAuthToken instead
If we can get an auth token for our home server, then the user must have access to it.

plexUserHasAccessToPlexHost method wouldn't work if our plex server had zero allowed users (except ourselves), since the data would not appear for ourself.
2024-03-18 01:40:45 +00:00
IRHM 4fdb3e9f9d 1.36.0-dev 2024-03-17 18:48:57 +00:00
Mr c1be94738b Jobs: Use wildcard for get job route id param (#415)
Fixes 404 errors when there is a '/' in the job id.
2024-03-17 18:22:27 +00:00
IRHM ea66615f65 plex: Remove info log 2024-03-17 17:39:00 +00:00
IRHM 5b38f762ed Remove old plex remnants
- $plex scss variable
- plexOauthId property in AvailableAuthProviders interface
2024-03-17 17:30:36 +00:00
dependabot[bot] 2aa81f036e Bump follow-redirects from 1.15.5 to 1.15.6 in /doc (#414)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-17 17:18:07 +00:00
Mr cbf0120647 Merge pull request #412 from sbondCo/dependabot/npm_and_yarn/follow-redirects-1.15.6
ui: bump follow-redirects from 1.15.4 to 1.15.6
2024-03-17 17:17:49 +00:00
Mr 2865115b1f Merge pull request #413 from sbondCo/plex-sync
Plex sync
2024-03-17 17:13:19 +00:00
IRHM b21d2bf603 Plex: Move auth tokens to new UserServices table and get local auth tokens 2024-03-17 17:04:10 +00:00
IRHM 0667f33250 plex sync: dont parse lastViewedAt for seasons/episodes if not set 2024-03-16 23:37:44 +00:00
IRHM 0da264480c plex auth: update auth token on login
hm mite save twice on creation
2024-03-16 23:23:39 +00:00
IRHM 622d10e743 Plex sync seasons and episodes 2024-03-16 23:18:32 +00:00
dependabot[bot] 5cbc817089 ui: bump follow-redirects from 1.15.4 to 1.15.6
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.4 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-16 22:36:17 +00:00
IRHM 7c216f4691 Plex sync movies and shows
Based on #401 - Seemed easier to port the relevant changes with some modifications than to fix merge conflicts and mess with history.

Co-Authored-By: stephaje <57157278+stephaje@users.noreply.github.com>
2024-03-16 22:26:01 +00:00
dependabot[bot] b5f9700345 ui: bump @typescript-eslint/eslint-plugin from 7.0.2 to 7.2.0 (#407)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 7.0.2 to 7.2.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.2.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-12 19:59:37 +00:00
Mr 7e03557e56 README: Add matrix chat badge to top and text in getting help section 2024-03-11 21:08:23 +00:00
dependabot[bot] 10a79a31cd server: bump github.com/gin-contrib/cors from 1.5.0 to 1.7.0 in /server (#403)
Bumps [github.com/gin-contrib/cors](https://github.com/gin-contrib/cors) from 1.5.0 to 1.7.0.
- [Release notes](https://github.com/gin-contrib/cors/releases)
- [Changelog](https://github.com/gin-contrib/cors/blob/master/.goreleaser.yaml)
- [Commits](https://github.com/gin-contrib/cors/compare/v1.5.0...v1.7.0)

---
updated-dependencies:
- dependency-name: github.com/gin-contrib/cors
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 20:28:42 +00:00
dependabot[bot] e41d408cd0 ui: bump eslint from 8.56.0 to 8.57.0 (#378)
Bumps [eslint](https://github.com/eslint/eslint) from 8.56.0 to 8.57.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.56.0...v8.57.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 20:27:40 +00:00
dependabot[bot] 6b22b4a4e2 server: bump golang.org/x/crypto from 0.19.0 to 0.21.0 in /server (#393)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.19.0 to 0.21.0.
- [Commits](https://github.com/golang/crypto/compare/v0.19.0...v0.21.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 20:26:52 +00:00
dependabot[bot] 67c3083edd server: bump github.com/golang-jwt/jwt/v5 from 5.2.0 to 5.2.1 in /server (#404)
Bumps [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) from 5.2.0 to 5.2.1.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v5.2.0...v5.2.1)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 20:26:25 +00:00
dependabot[bot] e968b7ff28 ui: bump @typescript-eslint/parser from 7.0.2 to 7.2.0 (#406)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 7.0.2 to 7.2.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.2.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 20:25:39 +00:00
dependabot[bot] adfd983a68 ui: bump @sveltejs/kit from 2.5.0 to 2.5.3 (#405)
Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 2.5.0 to 2.5.3.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.5.3/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 20:25:08 +00:00
dependabot[bot] b32ba86c5d ui: bump vite from 5.1.3 to 5.1.6 (#408)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.1.3 to 5.1.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.1.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 20:23:57 +00:00
Mr ebb0b132bd plex auth improvements (#402)
- Our own auth flow, removed 'plex-oauth' dep.
- Fixed normal registrations
- Combined login/register plex methods
- Changed login page plex btn styling
- Reverted colors on jellyfin/plex btns (couldn't find a nice one and it hurt me)
- Removed plex client id from server config, replaced by plex host and plex machine id config items
- Plex client id is generated on the client and stored for reuse, each client will do that instead of the server storing it for everyone.
- Use plex machine id to ensure only approved users can login via plex (users with access to host server library)
- Renamed some structs and funcs
- Removed background from plex icon
- Made buttons and inputs bigger (more padding, also unrelated to rest of changed but i think it looks nice)
2024-03-11 03:21:03 +00:00
stephaje 4f1fbf4a45 Plex Auth (#387) 2024-03-10 00:49:04 +00:00
IRHM 74f896a8bc docs: Fix viewjs css not loading in build 2024-03-09 03:42:22 +00:00
IRHM 3b1d42c148 docs: Screenshots (+ viewerjs for viewing them closer), demo link in nav 2024-03-09 01:55:06 +00:00
IRHM 7a902212a5 v1.35.2 2024-03-07 11:24:34 +00:00
IRHM 2a718ef6d8 #400 Fix signup_enabled default config value
Set defaults when generating config so they are stored in the json file.
2024-03-07 11:23:56 +00:00
IRHM f71dc287bd v1.35.1 2024-03-06 23:29:06 +00:00
IRHM 77d2c6f49a 1.35.1-dev 2024-03-06 23:10:29 +00:00
Mr 3eb454cc93 Config: Don't omit SIGNUP_ENABLED when empty (#399)
Was being removed from config when set to false, then once the server restarts would be set back to default value (true).

Now is kept in config with value of false when turned off.
2024-03-06 22:40:50 +00:00
Mr 8a5fd8182f Add restart property to docker-compose example (#398)
* #390

and update docker-compose file in proj root to match one in docs

* docker-compose.yml: fix port

i changed this :(
2024-03-06 22:31:47 +00:00
IRHM aa7a38b2ee v1.35.0 2024-03-05 20:50:09 +00:00
IRHM ed7a780c44 1.35.0-dev 2024-03-05 18:57:19 +00:00
Mr 32f72f382e Merge pull request #395 from sbondCo/jellyfin-sync
Jellyfin sync improvements
2024-03-05 18:20:34 +00:00
IRHM 2e0ea4d6c7 seasonAndEpToReadable: Remove padStart and fix showing season 0 as unknown
season 0 = specials, dont want to show 'unknown' in this case.
2024-03-05 17:54:23 +00:00
IRHM 2a9a3691a3 Jellyfin Sync: Add full_item debug log for episodes and seasons 2024-03-05 17:41:14 +00:00
IRHM 49692f5e02 Jellyfin Sync: Save episode/season watch dates
and change activity to show 'synced' instead of 'imported' for JF types
2024-03-05 17:38:44 +00:00
Mr 5f06e7b52f Merge pull request #394 from sbondCo/jellyfin-sync
Jellyfin (Manual) Sync
2024-03-05 01:04:19 +00:00
IRHM 1933ca350d jobs: Remove job after 1m of status being set to done 2024-03-05 01:03:00 +00:00
IRHM 337957fb9f Jellyfin Sync: _JF activity types
Not doing anything with them now, but later we can add to the UI (maybe jf icon if _JF activity variant)
2024-03-05 00:33:52 +00:00
IRHM 458c91d505 Jellyfin Sync: Don't add imported date activity if movie already on list 2024-03-05 00:00:08 +00:00
IRHM 5498236b48 JellyfinSyncModal: Stop job watcher after 10 errors in a row 2024-03-04 23:49:45 +00:00
IRHM 0e7cdb0199 watched episodes/seasons: Don't add activity if we didn't update the status/rating 2024-03-04 20:35:55 +00:00
IRHM 7d25b0bf5d wip: jellyfin sync modal 2024-03-04 03:50:41 +00:00
IRHM ccadcf2152 wip: jellyfin sync 2024-03-03 22:53:05 +00:00
IRHM 8e0fb190d1 ui: bump @typescript-eslint (eslint-plugin and parser) to 7.0.2 2024-02-22 02:43:03 +00:00
dependabot[bot] 62033c66aa ui: bump svelte-check from 3.6.3 to 3.6.4 (#370)
Bumps [svelte-check](https://github.com/sveltejs/language-tools) from 3.6.3 to 3.6.4.
- [Release notes](https://github.com/sveltejs/language-tools/releases)
- [Commits](https://github.com/sveltejs/language-tools/compare/svelte-check-3.6.3...svelte-check-3.6.4)

---
updated-dependencies:
- dependency-name: svelte-check
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-22 02:36:12 +00:00
dependabot[bot] 95130fd4fb ui: bump prettier-plugin-svelte from 3.1.2 to 3.2.1 (#368)
Bumps [prettier-plugin-svelte](https://github.com/sveltejs/prettier-plugin-svelte) from 3.1.2 to 3.2.1.
- [Changelog](https://github.com/sveltejs/prettier-plugin-svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/prettier-plugin-svelte/compare/v3.1.2...v3.2.1)

---
updated-dependencies:
- dependency-name: prettier-plugin-svelte
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-22 02:35:54 +00:00
dependabot[bot] c4cebb717c ui: bump @typescript-eslint/eslint-plugin from 6.21.0 to 7.0.0 (#367)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.21.0 to 7.0.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.0.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-22 02:35:21 +00:00
dependabot[bot] d92dd51bc8 ui: bump vite from 5.0.12 to 5.1.3 (#369)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.0.12 to 5.1.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.1.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-22 02:34:55 +00:00
Mr 4e4f8f9fcf ActivityEditor: Accept any valid date (#372)
* Activity: Allow any valid date in editor, add try/catch to updateActivity method

* api: Update updateActivity log
2024-02-22 02:33:34 +00:00
Mr 11a0ec55d5 Merge pull request #366 from stephaje/Issue-#61
Issue #61 - Editing activity
2024-02-22 01:48:46 +00:00
John Stephani 14b3f38b5d Add client validation 2024-02-21 19:17:13 -06:00
stephaje 5799cd649b context => c 2024-02-19 12:44:11 -06:00
stephaje d4b873ec7d Clean up errors, CSS, fix tv/games activity 2024-02-18 23:26:31 -06:00
stephaje d00ae04074 Add user id checking; fix log/error messages 2024-02-18 14:00:07 -06:00
stephaje 8955ca7b26 npm run format 2024-02-18 10:52:17 -06:00
stephaje 82c6729a22 Issue #61 2024-02-18 10:47:16 -06:00
IRHM 1929272705 v1.34.0 2024-02-18 16:45:56 +00:00
IRHM 709dead520 1.34.0-dev 2024-02-18 16:21:40 +00:00
Mr 7cd348804c Merge pull request #365 from sbondCo/fix-discovery
Fix discovery (trending) requests
2024-02-18 16:20:25 +00:00
IRHM ba91f65cee #364 Fix discovery requests
Trailing / causing 404
2024-02-18 16:18:59 +00:00
Mr a09488dbba Merge pull request #363 from sbondCo/rate-episodes
Ratings and Statuses for episodes
2024-02-18 16:08:23 +00:00
IRHM 111b2b1afd getLatestWatchedInTv: Fix undefined check, should be looking for bigger than -1 2024-02-18 16:04:13 +00:00
IRHM c0ff3205a6 Include episode in watching season extra detail 2024-02-18 15:43:59 +00:00
IRHM 92815f6907 #42 Episode Ratings 2024-02-17 23:15:43 +00:00
dependabot[bot] d9257e767f server: bump gorm.io/driver/sqlite from 1.5.4 to 1.5.5 in /server (#353)
Bumps [gorm.io/driver/sqlite](https://github.com/go-gorm/sqlite) from 1.5.4 to 1.5.5.
- [Commits](https://github.com/go-gorm/sqlite/compare/v1.5.4...v1.5.5)

---
updated-dependencies:
- dependency-name: gorm.io/driver/sqlite
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-16 23:12:43 +00:00
dependabot[bot] 26ec0f592a server: bump golang.org/x/crypto from 0.18.0 to 0.19.0 in /server (#354)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.18.0 to 0.19.0.
- [Commits](https://github.com/golang/crypto/compare/v0.18.0...v0.19.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-16 23:11:09 +00:00
dependabot[bot] 1e4814d50a server: bump gorm.io/gorm from 1.25.6 to 1.25.7 in /server (#355)
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.25.6 to 1.25.7.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.25.6...v1.25.7)

---
updated-dependencies:
- dependency-name: gorm.io/gorm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-16 23:10:49 +00:00
dependabot[bot] bbe2123c1e ui: bump prettier from 3.2.4 to 3.2.5 (#341)
Bumps [prettier](https://github.com/prettier/prettier) from 3.2.4 to 3.2.5.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.2.4...3.2.5)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-16 23:07:04 +00:00
dependabot[bot] d7c2897698 ui: bump @typescript-eslint/eslint-plugin from 6.20.0 to 6.21.0 (#338)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.20.0 to 6.21.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.21.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-16 23:05:45 +00:00
dependabot[bot] 8cd930369d ui: bump svelte from 4.2.8 to 4.2.10 (#337)
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 4.2.8 to 4.2.10.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/svelte@4.2.10/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@4.2.10/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-16 23:04:47 +00:00
dependabot[bot] 4cea72d8e9 ui: bump axios from 1.6.5 to 1.6.7 (#339)
Bumps [axios](https://github.com/axios/axios) from 1.6.5 to 1.6.7.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.6.5...v1.6.7)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-16 23:04:13 +00:00
imgbot[bot] e9a4629c2a [ImgBot] Optimize images (#357)
*Total -- 936.02kb -> 774.21kb (17.29%)

/doc/docs/server_config/img/twitch-register-app.png -- 80.15kb -> 58.27kb (27.29%)
/doc/docs/server_config/img/watcharr-settings-dropdown.png -- 8.20kb -> 6.11kb (25.44%)
/doc/docs/server_config/img/watcharr-twitch-config.png -- 34.31kb -> 26.69kb (22.2%)
/doc/docs/server_config/img/watcharr-twitch-service.png -- 10.34kb -> 8.17kb (21.04%)
/doc/docs/server_config/img/watcharr-game-details.png -- 803.03kb -> 674.97kb (15.95%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
2024-02-16 15:52:28 +00:00
Mr 3e02af9c40 README: Update description, adding video games. 2024-02-12 01:01:37 +00:00
IRHM 2a5a470347 v1.33.0 2024-02-12 00:33:07 +00:00
IRHM 07e3f5f7a8 1.33.0-dev 2024-02-11 23:44:07 +00:00
Mr 1b83c70b5e Merge pull request #344 from sbondCo/twitch-config-modal-doc-link
TwitchModal: Add link to setup docs.
2024-02-11 23:41:09 +00:00
IRHM 1e5cdc151b TwitchModal: Add link to setup docs. 2024-02-11 23:37:53 +00:00
Mr dcb21a6e1e Merge pull request #343 from sbondCo/game-docs
Game docs
2024-02-11 23:30:41 +00:00
IRHM 0b4f3d5c97 Create game setup guide
i just realised screenshots are in dark mode, not matching the others in another guide, but it's too late to change em cuz what's done is done.
2024-02-11 23:29:14 +00:00
IRHM 0d8ea359e4 doc: Update docusaurus to v3 2024-02-11 22:41:51 +00:00
Mr 1c44b1812a Merge pull request #342 from sbondCo/games
Games
2024-02-11 21:56:58 +00:00
IRHM 3d5cf70ec0 FilterMenu: Change held to on hold 2024-02-10 20:26:35 +00:00
IRHM f1f26d0f57 Dockerfile: Copy game module 2024-02-10 19:54:18 +00:00
IRHM e3fe047afd follow: Fix get watched item logic for followed thoughts
Before it could get two entries if user has two entries that had same id number for content and game ids.
2024-02-10 17:50:10 +00:00
IRHM 4afc32f9d1 FilterMenu: Fix type styling 2024-02-10 17:49:10 +00:00
IRHM 7ea78f2d57 watched: preload game when getting public list 2024-02-10 17:31:10 +00:00
IRHM 6092410b20 Followed thoughts for games 2024-02-10 17:28:29 +00:00
IRHM 8f5293a769 Remove game filter if games disabled, update status filters for games 2024-02-10 16:48:25 +00:00
IRHM 6d25356d30 WatchedList: Fix type filtering 2024-02-10 16:18:14 +00:00
IRHM dbd86b89c7 FilterMenu: Add Game type 2024-02-10 16:18:07 +00:00
IRHM bf0b4fbb6a game: remove log of credentials 2024-02-10 15:56:35 +00:00
IRHM 5585053b85 igdb token refresh logic 2024-02-10 15:53:30 +00:00
IRHM a61b82e9af Search: Fix if games, catch search requests separately 2024-02-08 22:55:30 +00:00
IRHM d4fe6d3653 Search: Remove unused interface, remove comment 2024-02-08 22:45:30 +00:00
IRHM 6d2ab6e7e4 Search: Only search movies/tv if games disabled 2024-02-08 22:44:46 +00:00
IRHM c5e6ae702a Poster: Don't try loading image unless we have a poster path 2024-02-08 22:44:27 +00:00
IRHM 2a9b0443f5 Tv: Fix wListItem find 2024-02-08 00:10:55 +00:00
IRHM e1e933af16 Fix Status 2024-02-08 00:08:39 +00:00
IRHM a5fbecd32a Remove unneeded comments 2024-02-07 23:54:32 +00:00
IRHM 98006efb03 Games: Fix similar games having wrong status/rating 2024-02-07 23:49:11 +00:00
IRHM 926b0f8267 GamePoster: Use blurhash when available for poster loader 2024-02-07 23:46:59 +00:00
IRHM cc3f21aa93 Cache game covers 2024-02-07 22:47:16 +00:00
IRHM e311bcd901 Display status differently for games
Created toUnderstandableStatus and toUnderstandableWatchedStatus helpers to convert statuses into readable.
2024-02-06 23:56:49 +00:00
IRHM eb66baba80 Routes: Remove testing cache exp time for game routes 2024-02-03 21:41:26 +00:00
IRHM 09795b4da3 Game: Display similar games 2024-02-03 21:41:03 +00:00
IRHM 8088184e6b Create getPlayedDependedProps for searched game posters 2024-02-03 21:09:04 +00:00
IRHM 1b6c56dd8f sorta finalize gameposter on own watched list 2024-02-03 21:00:26 +00:00
IRHM 86195eb682 Game: Add check before trying to iterate over genres and modes
and show 'Unknown' message if we have none available
2024-02-03 20:38:12 +00:00
IRHM f40ffa124c GamePoster: Accept generic props so we can reuse on watched list 2024-02-03 20:38:12 +00:00
IRHM 704b6380c7 Game: Use cover if no artwork available for backdrop 2024-02-03 20:38:12 +00:00
IRHM b434665caf fix content undefined issues 2024-02-03 20:38:12 +00:00
IRHM 3554d4bc60 updating games watched item 2024-02-03 20:38:12 +00:00
IRHM 917ddd5401 fix profile stats
since Content pointer stored now
2024-02-03 20:38:12 +00:00
IRHM e7cf21fdb2 basic adding games to list 2024-02-03 20:33:52 +00:00
IRHM c932bd188a basic game detail page 2024-02-03 20:32:44 +00:00
IRHM e5d17bee81 game search ep 2024-02-03 20:31:45 +00:00
IRHM 65836a1654 idk if this works
bakup the beginnings of game support
2024-02-03 20:31:45 +00:00
IRHM 0f7bdaffbb v1.32.1 2024-02-03 15:16:09 +00:00
IRHM 30dae4c5c9 1.32.1-dev1 2024-02-03 14:57:28 +00:00
Mr f67d1641db Merge pull request #336 from sbondCo/awesome-stuff-too
Fix movary importing and include previously watched filter
2024-02-03 14:54:39 +00:00
IRHM 0ff02e00c9 WatchedList: Fix type filter when finished status enabled 2024-02-03 13:37:24 +00:00
IRHM 194081ac2f Profile: get profile stats again after updating includepreviouslywatched setting 2024-02-03 13:34:24 +00:00
IRHM 896f80ebd6 profile: Include previously watched in stats if enabled by user 2024-02-03 13:16:55 +00:00
IRHM 862279e914 import: Fix customDates being the same on all dateswatched activity
pointer value being updated each iteration, so we end up using same date for all object in the array
2024-02-03 12:51:17 +00:00
IRHM bdf5cf54af 1.32.1-dev 2024-02-03 02:53:16 +00:00
Mr 4323ab7086 Merge pull request #335 from sbondCo/amazing-stuff
https://github.com/sbondCo/Watcharr/issues/332#issuecomment-1923230972
2024-02-03 02:51:26 +00:00
IRHM 555b237ed0 WatchedList: Filter on sort change, add STATUS_CHANGED as activity that could make show watched 2024-02-03 02:37:58 +00:00
IRHM 5146af2672 Filtering: Add includePreviouslyWatched setting 2024-02-03 02:13:46 +00:00
IRHM 4d3da51ae7 Movary Import: Keep history data if show is also on watchlist 2024-02-02 21:35:54 +00:00
IRHM 9cba877fe6 Movary Import: Set latest watched date as watched entry CreatedAt 2024-02-02 21:21:21 +00:00
IRHM 924ffcb33f v1.32.0 2024-02-02 02:24:50 +00:00
IRHM b638062d36 1.32.0-dev 2024-02-02 01:47:20 +00:00
dependabot[bot] da64b5ca59 ui: bump svelte-check from 3.6.2 to 3.6.3 (#317)
Bumps [svelte-check](https://github.com/sveltejs/language-tools) from 3.6.2 to 3.6.3.
- [Release notes](https://github.com/sveltejs/language-tools/releases)
- [Commits](https://github.com/sveltejs/language-tools/compare/svelte-check-3.6.2...svelte-check-3.6.3)

---
updated-dependencies:
- dependency-name: svelte-check
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 01:30:51 +00:00
dependabot[bot] 8f2d9b0984 ui: bump @typescript-eslint/parser from 6.18.0 to 6.20.0 (#328)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.18.0 to 6.20.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.20.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 01:30:34 +00:00
dependabot[bot] c2e85ff8b3 server: bump gorm.io/gorm from 1.25.5 to 1.25.6 in /server (#327)
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.25.5 to 1.25.6.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.25.5...v1.25.6)

---
updated-dependencies:
- dependency-name: gorm.io/gorm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 01:29:18 +00:00
dependabot[bot] 3c9d074169 ui: bump @typescript-eslint/eslint-plugin from 6.18.0 to 6.20.0 (#330)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.18.0 to 6.20.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.20.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 01:28:38 +00:00
dependabot[bot] 7a254222a1 ui: bump @sveltejs/adapter-node from 2.0.2 to 4.0.1 (#331)
Bumps [@sveltejs/adapter-node](https://github.com/sveltejs/kit/tree/HEAD/packages/adapter-node) from 2.0.2 to 4.0.1.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/adapter-node/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/adapter-node@4.0.1/packages/adapter-node)

---
updated-dependencies:
- dependency-name: "@sveltejs/adapter-node"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 01:26:35 +00:00
dependabot[bot] 4662977677 ui: bump prettier from 3.1.1 to 3.2.4 (#322)
Bumps [prettier](https://github.com/prettier/prettier) from 3.1.1 to 3.2.4.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.1.1...3.2.4)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 01:25:01 +00:00
dependabot[bot] 798c80f69b ui: bump vite from 5.0.11 to 5.0.12 (#320)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.0.11 to 5.0.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.0.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.0.12/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 01:24:29 +00:00
Mr d8e4182824 Merge pull request #334 from sbondCo/movary-import
Movary Importing
2024-02-02 01:21:49 +00:00
IRHM ac9fb8c28d import/process: Update importText for TMDb to TMDB 2024-02-02 01:11:32 +00:00
IRHM d0b1b9ed17 import: add error to sucimp log 2024-02-02 00:56:16 +00:00
IRHM 548643327d Support importing status, thoughts and datesWatched
for movary import
2024-02-02 00:45:36 +00:00
IRHM a86d2a218a wip build movary import list 2024-02-02 00:18:41 +00:00
IRHM 21ecc4cbbf wip movary importing from the 3 export files 2024-02-01 01:32:01 +00:00
Mr 2e19cad3a8 Merge pull request #314 from sbondCo/update-cache-on-req
Update content table on request
2024-01-13 22:47:48 +00:00
IRHM 8bffc7380e saveContent: remove test log and debug on Updates
and update its error handling, copied from below, removed case not needed for updating... or even that code anymore but whatevz
2024-01-13 22:44:02 +00:00
IRHM 96720caace saveContent: Support onlyUpdate parameter 2024-01-13 17:40:33 +00:00
IRHM 6c130952d7 update cache when content is freshly fetched 2024-01-11 02:29:46 +00:00
IRHM 3c1fc0e950 split out content caching for reuse 2024-01-11 02:05:35 +00:00
IRHM d95990fc56 v1.31.1 2024-01-09 20:45:59 +00:00
dependabot[bot] 9d0a433121 Bump follow-redirects from 1.15.3 to 1.15.4 in /doc (#311)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 20:44:50 +00:00
Mr 2bd02439c2 Support ARM64 Arch (#310)
* dev workflow: Add arm64 platform build

in sequence because paralellising the jobs looks hard

* dev action: remove `dev` tag

I think it would be better to just refer to them with their sha256 instead + i dont like that when a `dev` image is built, the `dev` tag goes to top of list of tagged images

* Revert "dev action: remove `dev` tag"

This reverts commit 0179f616fa.

nevermind, that is not a thing.

* release action: add arm platform
2024-01-09 20:42:30 +00:00
dependabot[bot] 6a1fdd69e3 ui: bump @sveltejs/kit from 2.0.7 to 2.0.8 (#308)
Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 2.0.7 to 2.0.8.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.0.8/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-08 20:35:40 +00:00
dependabot[bot] fb7dc5d0f8 ui: bump axios from 1.6.3 to 1.6.5 (#304)
Bumps [axios](https://github.com/axios/axios) from 1.6.3 to 1.6.5.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.6.3...v1.6.5)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-08 20:34:12 +00:00
dependabot[bot] c3d480d47b ui: bump @sveltejs/kit from 2.0.6 to 2.0.7 (#305)
Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 2.0.6 to 2.0.7.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.0.7/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-08 20:33:39 +00:00
dependabot[bot] 274c97bcdd ui: bump @typescript-eslint/eslint-plugin from 6.17.0 to 6.18.0 (#306)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.17.0 to 6.18.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.18.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-08 20:33:20 +00:00
dependabot[bot] d6b5fc9cd3 server: bump golang.org/x/crypto from 0.17.0 to 0.18.0 in /server (#303)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.17.0 to 0.18.0.
- [Commits](https://github.com/golang/crypto/compare/v0.17.0...v0.18.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-08 20:32:51 +00:00
dependabot[bot] 4de9865a39 ui: bump vite from 5.0.10 to 5.0.11 (#307)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.0.10 to 5.0.11.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.0.11/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-08 20:32:22 +00:00
Mr d278f2cf98 Merge pull request #302 from sbondCo/watchtime
Watchtime Profile Stat
2024-01-07 23:41:21 +00:00
IRHM 5aec83c2f0 Profile: Badly implement show watchtime
for now... hopefully
2024-01-07 23:36:56 +00:00
IRHM fbda3d4b9b Stat: Add disclaimer prop 2024-01-07 23:15:52 +00:00
IRHM dde8fad248 Profile: Implement movies watched runtime stat 2024-01-07 21:52:26 +00:00
IRHM e8309568bc v1.31.0 2024-01-07 17:50:51 +00:00
IRHM 997e3afb7e 1.31.0-dev 2024-01-07 01:07:39 +00:00
Mr 891fc51bcd Input changed validation (#299)
* thoughts change validation

* Rating & Status change validation

* Poster: status change validation
2024-01-07 01:04:00 +00:00
dependabot[bot] ebe42f1d34 ui: bump @typescript-eslint/parser from 6.16.0 to 6.18.0 (#298)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.16.0 to 6.18.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.18.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-06 18:43:04 +00:00
Mr b799c660ca Merge pull request #294 from sbondCo/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-6.17.0
ui: bump @typescript-eslint/eslint-plugin from 6.16.0 to 6.17.0
2024-01-06 18:41:14 +00:00
Mr a10f570983 Merge pull request #293 from sbondCo/dependabot/npm_and_yarn/axios-1.6.3
ui: bump axios from 1.6.2 to 1.6.3
2024-01-06 18:40:51 +00:00
Mr f205f96010 Merge pull request #297 from sbondCo/detail-view
Detail view
2024-01-06 18:39:49 +00:00
IRHM 735a98e0b2 Poster: Make extra details box more opaque
and use same color for both themes
2024-01-06 17:45:47 +00:00
IRHM f87aa70a3b Poster: fix extra details colors and spacing 2024-01-06 17:41:13 +00:00
IRHM a2dc418ee6 Poster: Hide empty extra-details 2024-01-05 19:35:39 +00:00
IRHM 4c7982f7be improve getLatestWatchedSeason
it could be improved more though
2024-01-05 06:17:41 +00:00
IRHM 43d603c212 Detailed Poster: Use real data 2024-01-05 05:39:49 +00:00
IRHM ebf623ed1b Redesign poster extra details and allow configuring displayed data 2024-01-05 04:53:00 +00:00
IRHM d925519beb Create DetailedPoster 2024-01-03 23:13:38 +00:00
dependabot[bot] 07b72452e8 ui: bump @typescript-eslint/eslint-plugin from 6.16.0 to 6.17.0
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.16.0 to 6.17.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.17.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-01 19:22:43 +00:00
dependabot[bot] ab4781bec0 ui: bump axios from 1.6.2 to 1.6.3
Bumps [axios](https://github.com/axios/axios) from 1.6.2 to 1.6.3.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.6.2...v1.6.3)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-01 19:21:56 +00:00
IRHM 7719b6336c v1.30.0 2024-01-01 02:50:19 +00:00
IRHM e749e94bfb 1.30.0-dev 2023-12-31 20:23:17 +00:00
IRHM c1ceb17ada Rating: Fix text not updating if network request resolves after we hoverEnd 2023-12-31 20:18:39 +00:00
IRHM 91dd00b556 routes: Set AuthRequired middleware once for auth routes that require it
indenting authd routes definitely helps with readability
2023-12-30 17:37:59 +00:00
Mr 60e873b4c0 Merge pull request #292 from sbondCo/content-stars-text
Content rating text follow hovered star
2023-12-30 05:02:28 +00:00
IRHM 4333591c9c Rating: Add transition to transform
to make it smoother when moving to initial close rating to default middle pos.
2023-12-30 04:53:10 +00:00
IRHM 462a46aabb Rating: Fix causing page overflow on small viewports 2023-12-30 04:52:15 +00:00
IRHM c766d68d49 Rating: Center desc text and make it follow hovered star 2023-12-29 03:04:56 +00:00
Mr 6b7c38aa7a Merge pull request #291 from sbondCo/exportwl
Export Watched List
2023-12-29 02:08:48 +00:00
IRHM 367c79f29c Change empty watch list error text 2023-12-29 02:07:35 +00:00
IRHM c59eb82a1a dont try saving an empty watch list 2023-12-29 02:03:46 +00:00
IRHM 3d40e5554f change suggested export name to watcharr-export.json 2023-12-29 01:58:49 +00:00
IRHM 75f091cfa0 Basic exporting watched list support 2023-12-29 01:55:07 +00:00
Mr 1d0abe4982 Merge pull request #290 from iamericfletcher/user-password-change-issue-279
Implement Change Password Feature (Issue #279)
2023-12-28 22:24:01 +00:00
Mr f8490481a5 Merge branch 'dev' into user-password-change-issue-279 2023-12-28 22:16:54 +00:00
IRHM 1adc56f965 userChangePassword: Select only pass from db 2023-12-28 22:12:50 +00:00
IRHM 8861233aaa Move GetPassArgonParams, Remove DEBUG, add use of ternary operator 2023-12-28 22:01:58 +00:00
iamericfletcher 3c4718bc45 Update error handling in changeUserPassword and display of error messages in pwChangeModal
The 'changeUserPassword' callback is updated such that error messages are passed back to the calling function in `PwChangeModal` allowing errors to show up in the same place.
2023-12-28 16:19:31 -05:00
iamericfletcher f6fa9e7666 Refactor ArgonParams to a function for register and userChangePassword in auth.go 2023-12-28 15:37:11 -05:00
iamericfletcher 98a7c370a8 Removed unneeded else branches in auth.go 2023-12-28 14:59:57 -05:00
iamericfletcher 37f9bffb13 Move server endpoint to /auth/change_password and update corresponding frontend API call 2023-12-28 14:52:19 -05:00
iamericfletcher a4573fe52a Move userChangePassword function to auth.go 2023-12-28 14:40:35 -05:00
iamericfletcher aeaaccda88 Remove Form.svelte and related import
Form.svelte was deleted due to not being used or needed. An associated import line in PwChangeModal.svelte has also been removed.

Additionally, setting parameter DEBUG was removed from server/config.go.
2023-12-27 17:48:18 -05:00
iamericfletcher 3a2ce0c718 Refactor password change feature and update debug messages
Removed a substantial block of commented-out code related to password changing in the `user.go` file, which simplifies the structure and readability of the code. Improved clarity and specificity of console debug/feedback messages throughout the frontend, ensuring easier debugging. Commit also includes minor code clean up in some files.
2023-12-27 17:24:05 -05:00
iamericfletcher 2ac80d0b8c Refine password change error handling and reporting
The commit delivers improved error handling and debugging information for password change functionality. Error messages now provide more detailed context on failures. Additionally, new password hashing has been added in the password change process. The user interface has also been updated to reflect specific error messages returned from the API.
2023-12-27 14:14:53 -05:00
iamericfletcher 3ff5d9836b Refactor user password change functionality
The password change function now involves first querying user data from the database, before checking the validity of their old password. Logic has been updated on both frontend and backend code. Errors have been added for handling instances where password comparison, user retrieval or other processes fail.
2023-12-27 10:08:06 -05:00
iamericfletcher e6d486c2a2 Implement password change feature
A new feature has been added that allows users to change their password. This includes updates in several different files to accommodate this new functionality. Changes include adding a new API endpoint, creating a new function to handle the password change request, and removing the username field from the ChangePasswordForm interface.
2023-12-26 20:43:11 -05:00
iamericfletcher c4e591e2f2 Improve form validation in PwChangeModal.svelte
Updated form validation to check for missing inputs and ensure consistency between new password and re-entered new password. Enhanced logging for better status tracking and error reporting during form validation process.
2023-12-26 12:36:32 -05:00
iamericfletcher 0eb1a88103 Refactoring password button in PwChangeModal to be in line with button in SonarrModal 2023-12-26 11:58:47 -05:00
IRHM ffe6afe2d2 v1.29.0 2023-12-26 14:46:15 +00:00
iamericfletcher c767572905 Refactoring password change modal form 2023-12-26 09:36:24 -05:00
IRHM da803220e4 1.29.0-dev1 2023-12-26 14:23:57 +00:00
IRHM 5c4f023fd8 FollowedThoughts: Fix dark theme status icon fill 2023-12-26 14:23:31 +00:00
IRHM 7e2e64f394 1.29.0-dev 2023-12-26 14:14:07 +00:00
dependabot[bot] 9609909122 ui: bump @typescript-eslint/eslint-plugin from 6.13.0 to 6.16.0 (#285)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.13.0 to 6.16.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.16.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-26 14:09:02 +00:00
dependabot[bot] 7522784f18 ui: bump @typescript-eslint/parser from 6.15.0 to 6.16.0 (#284)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.15.0 to 6.16.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.16.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-26 14:07:09 +00:00
dependabot[bot] 8d5379e38b ui: bump svelte-preprocess from 5.1.1 to 5.1.3 (#283)
Bumps [svelte-preprocess](https://github.com/sveltejs/svelte-preprocess) from 5.1.1 to 5.1.3.
- [Changelog](https://github.com/sveltejs/svelte-preprocess/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte-preprocess/compare/v5.1.1...v5.1.3)

---
updated-dependencies:
- dependency-name: svelte-preprocess
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-26 14:06:43 +00:00
dependabot[bot] 8065349a5c ui: bump eslint from 8.55.0 to 8.56.0 (#286)
Bumps [eslint](https://github.com/eslint/eslint) from 8.55.0 to 8.56.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.55.0...v8.56.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-26 14:04:50 +00:00
dependabot[bot] 26c84ae7c7 ui: bump prettier from 3.1.0 to 3.1.1 (#287)
Bumps [prettier](https://github.com/prettier/prettier) from 3.1.0 to 3.1.1.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.1.0...3.1.1)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-26 14:04:07 +00:00
Mr f190bffdd0 Merge pull request #288 from sbondCo/public-thoughts
Public thoughts
2023-12-26 06:36:08 +00:00
IRHM e0f8d753bf follow: remove debug log 2023-12-26 06:27:22 +00:00
IRHM b6223c7ac8 FollowedThoughts: Ensure status icon is always the same size 2023-12-26 06:08:12 +00:00
IRHM ffd8bbe442 expose new privateThoughts setting to user 2023-12-26 05:45:54 +00:00
IRHM bc86b8f6b4 FollowedThoughts: Add extra space between status and rating 2023-12-26 05:26:12 +00:00
IRHM 0cb5c01b5e FollowedThoughts: Add status icon to cards 2023-12-26 05:22:39 +00:00
IRHM 0393966ecc getting and showing followed thoughts on content
Show followed users rating and thoughts on content
2023-12-26 05:04:41 +00:00
iamericfletcher b3ba00cd0d Added beginning stages of password change functionality in user profile
A new interface ChangePasswordForm has been created to handle password change form data.

The 'PwChangeModal.svelte' modal allows the user to input current and new passwords for a change password request.

The user profile page has been updated to include a "Change Password" button which triggers the change password modal.
2023-12-25 21:39:54 -05:00
Mr 5084a5b724 Merge pull request #281 from iamericfletcher/feature-sort-filter-watched-lists
Enhancement: Allow filtering and sorting other watched lists #276
2023-12-24 23:43:30 +00:00
iamericfletcher 43a484f949 Remove unused 'watchedListErrorOccurred' 2023-12-24 12:59:11 -05:00
iamericfletcher 8434e852ce Enhancement: Allow filtering and sorting other watched lists #276
Added a new store variable 'watchedListErrorOccurred' set to true if `getPublicWatchedList()` promise errors.

The aim is that the filter and sort functionality only show if the watched list loads in successfully.
2023-12-24 09:02:35 -05:00
Mr e541dc5cdb Merge pull request #280 from sbondCo/fix-dupe-tmdbids
Fix content with same tmdb ids
2023-12-23 20:29:41 +00:00
IRHM 4a20819803 addWatched: Check content type when fetching 2023-12-23 20:23:22 +00:00
IRHM 9a568b9da7 add content type to .find of wListItem in content pages 2023-12-23 20:15:13 +00:00
dependabot[bot] 9176d97eee ui: bump @sveltejs/kit from 1.27.6 to 2.0.6 (#278)
* ui: bump @sveltejs/kit from 1.27.6 to 2.0.6

Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 1.27.6 to 2.0.6.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.0.6/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* update @sveltejs/adapter-node, @vite-pwa/sveltekit, vite

to fix peer dep issues

* import vitePreprocess from @sveltejs/vite-plugin-svelte

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: IRHM <37304121+IRHM@users.noreply.github.com>
2023-12-23 16:26:06 +00:00
dependabot[bot] 1cbc065b7b ui: bump eslint-plugin-svelte from 2.35.0 to 2.35.1 (#263)
Bumps [eslint-plugin-svelte](https://github.com/sveltejs/eslint-plugin-svelte) from 2.35.0 to 2.35.1.
- [Release notes](https://github.com/sveltejs/eslint-plugin-svelte/releases)
- [Changelog](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/eslint-plugin-svelte/compare/v2.35.0...v2.35.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-svelte
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-23 15:55:52 +00:00
dependabot[bot] b4865aa789 ui: bump prettier-plugin-svelte from 3.0.3 to 3.1.2 (#262)
Bumps [prettier-plugin-svelte](https://github.com/sveltejs/prettier-plugin-svelte) from 3.0.3 to 3.1.2.
- [Changelog](https://github.com/sveltejs/prettier-plugin-svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/prettier-plugin-svelte/compare/v3.0.3...v3.1.2)

---
updated-dependencies:
- dependency-name: prettier-plugin-svelte
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-23 15:55:31 +00:00
dependabot[bot] 0c29927ef6 workflow: bump actions/setup-node from 3 to 4 (#271)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-23 15:54:54 +00:00
dependabot[bot] 75e499a070 ui: bump typescript from 5.3.2 to 5.3.3 (#264)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 5.3.2 to 5.3.3.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v5.3.2...v5.3.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-23 15:54:26 +00:00
dependabot[bot] fe589e7de5 workflow: bump actions/checkout from 3 to 4 (#272)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-23 15:50:41 +00:00
dependabot[bot] 9d7d6b0e3a ui: bump @typescript-eslint/parser from 6.13.1 to 6.15.0 (#274)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.13.1 to 6.15.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.15.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-23 15:49:00 +00:00
Mr 5fb720cca4 Merge pull request #275 from sbondCo/dependabot/go_modules/server/golang.org/x/crypto-0.17.0
server: bump golang.org/x/crypto from 0.16.0 to 0.17.0 in /server
2023-12-23 15:48:20 +00:00
Mr 2084b42098 Merge pull request #277 from sbondCo/fix-tooltips
Fix tooltips on scroll and from causing overflow on window resize
2023-12-23 15:43:52 +00:00
IRHM d26db7759f resetTooltipPos on window resize to fix overflow
resizing window smaller if tooltip is somewhere can cause overflow on page, reset its pos to top left on resize to avoid this.
2023-12-23 15:40:59 +00:00
IRHM 89311d4487 Revert "make tooltips fixed position"
This reverts commit 4d65a16584.
2023-12-23 15:36:29 +00:00
dependabot[bot] 72dca29d69 server: bump golang.org/x/crypto from 0.16.0 to 0.17.0 in /server
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.16.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.16.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-18 19:50:49 +00:00
imgbot[bot] 36b3bde00e [ImgBot] Optimize images (#268)
*Total -- 5,368.58kb -> 4,578.90kb (14.71%)

/static/logo-sqre.png -- 10.43kb -> 4.68kb (55.15%)
/screenshot/user-profile.png -- 47.51kb -> 36.22kb (23.78%)
/screenshot/content-details-page-dark.png -- 523.03kb -> 429.56kb (17.87%)
/screenshot/content-details-page.png -- 370.65kb -> 304.62kb (17.81%)
/static/logo-sqre-144.png -- 6.43kb -> 5.39kb (16.28%)
/static/logo-sqre-192.png -- 8.61kb -> 7.24kb (15.97%)
/screenshot/homepage-poster-hover.png -- 877.03kb -> 751.92kb (14.27%)
/screenshot/homepage-poster-change-status.png -- 865.78kb -> 743.03kb (14.18%)
/screenshot/discover-page.png -- 765.65kb -> 660.56kb (13.73%)
/screenshot/homepage.png -- 945.27kb -> 816.18kb (13.66%)
/screenshot/homepage-dark.png -- 948.18kb -> 819.52kb (13.57%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
2023-12-10 17:34:04 +00:00
IRHM 425f0da422 v1.28.0 2023-12-10 03:02:38 +00:00
IRHM 6773736113 1.28.0-dev4 2023-12-10 02:57:23 +00:00
IRHM 51ec248b67 add short_name to pwa manifest 2023-12-10 02:57:00 +00:00
IRHM 702084c83c 1.28.0-dev3 2023-12-10 02:51:51 +00:00
IRHM 2aa5f0c4fa Add name, description, bg col and category to pwa manifest config
the lowercase name is too annoying, must have the capital W. Hopefully the bg colour can fix issues where the icon isn't scaled up, but white was shown so it looked odd. That may mask the issue.
2023-12-10 02:51:22 +00:00
IRHM 359f5a372a 1.28.0-dev2 2023-12-10 02:34:53 +00:00
Mr 03a8397498 Merge pull request #267 from sbondCo/new-pwa-icons
new icons for pwa
2023-12-10 02:34:02 +00:00
IRHM 034e6b10e7 new icons for pwa 2023-12-10 02:31:38 +00:00
IRHM 8100c5f9a9 1.28.0-dev1 2023-12-09 23:23:43 +00:00
Mr 95c8f2d5b0 Merge pull request #266 from sbondCo/prs
PR Template and test workflow
2023-12-09 23:15:35 +00:00
IRHM be29170f6d Create test-pr.yml
to check for formatting / linting errors
2023-12-09 23:12:59 +00:00
IRHM f02699b089 fix @html linting errors
the contents come from trusted sources so we should be okay
2023-12-09 23:08:37 +00:00
IRHM 82b7a3d0f6 fix css linting errors 2023-12-09 22:57:46 +00:00
IRHM 8608ad4fd6 format readme 2023-12-09 22:51:17 +00:00
IRHM ab96107797 Create PULL_REQUEST_TEMPLATE.md 2023-12-09 22:47:19 +00:00
Mr a38291c9b9 Merge pull request #260 from sbondCo/dokrfl
Fix dockerfile server build
2023-12-09 22:43:28 +00:00
IRHM 67cfd03a1d run formatter 2023-12-09 22:40:18 +00:00
IRHM c4fa99b5aa Fix dockerfile server build for musl
Workaround from gh issue seems to fix the issue of build failing
2023-12-09 22:28:22 +00:00
Mr 1d904a7188 Merge pull request #259 from ishaanparlikar/feature/pwa
Feature/pwa
2023-12-09 19:31:03 +00:00
ishaanparlikar be8cee5378 removed jsdoc type inference 2023-12-10 00:31:47 +05:30
ishaanparlikar 128a3d4cc6 removed workbox dependancy 2023-12-10 00:29:51 +05:30
ishaanparlikar 610fb3d0c0 fixed indentation 2023-12-09 12:06:59 +05:30
ishaanparlikar 2dd01f296b Feature: Added PWA support for Watcharr #225 2023-12-09 12:02:20 +05:30
Mr 6e118085a9 Merge pull request #258 from sbondCo/episode-overview
SeasonsListEpisode: Fix spoilers
2023-12-09 02:21:26 +00:00
IRHM 2f3400d4d7 SeasonsListEpisode: Fix spoilers 2023-12-09 02:20:53 +00:00
Mr da787741d8 Merge pull request #257 from sbondCo/episode-overview
Episode overview and runtime
2023-12-09 02:16:51 +00:00
IRHM 97bdf2f89c SeasonsListEpisode: Quick fix patch hack code and reimplement fix and repair the star position to fix it 2023-12-09 02:15:30 +00:00
IRHM a8c8431715 SeasonsListEpisode: Add episode runtimes 2023-12-09 02:11:15 +00:00
IRHM 18e3e333c6 SeasonsListEpisode: Add overviews 2023-12-09 02:01:29 +00:00
Mr f7ddb1e14b Merge pull request #255 from ishaanparlikar/fix/#217
Hiding rating when hide spoiler is checked in profile
2023-12-08 22:34:32 +00:00
ishaanparlikar e5c513161e Merge branch 'dev' of https://github.com/ishaanparlikar/Watcharr into dev 2023-12-08 21:49:27 +05:30
ishaanparlikar 55235839ff Hiding rating when hide spoiler is checked in profile 2023-12-08 21:41:21 +05:30
dependabot[bot] e5536c5429 ui: bump svelte-preprocess from 5.1.0 to 5.1.1 (#247)
Bumps [svelte-preprocess](https://github.com/sveltejs/svelte-preprocess) from 5.1.0 to 5.1.1.
- [Changelog](https://github.com/sveltejs/svelte-preprocess/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte-preprocess/compare/v5.1.0...v5.1.1)

---
updated-dependencies:
- dependency-name: svelte-preprocess
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-07 21:02:53 +00:00
dependabot[bot] 02fc0ccb94 ui: bump eslint-config-prettier from 9.0.0 to 9.1.0 (#248)
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 9.0.0 to 9.1.0.
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v9.0.0...v9.1.0)

---
updated-dependencies:
- dependency-name: eslint-config-prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-07 21:00:25 +00:00
dependabot[bot] 83dcb85e5b server: bump github.com/golang-jwt/jwt/v5 from 5.1.0 to 5.2.0 in /server (#245)
Bumps [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) from 5.1.0 to 5.2.0.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v5.1.0...v5.2.0)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v5
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-07 21:00:05 +00:00
dependabot[bot] 155cd21b40 ui: bump svelte from 4.2.7 to 4.2.8 (#246)
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 4.2.7 to 4.2.8.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/svelte@4.2.8/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@4.2.8/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-07 20:57:33 +00:00
dependabot[bot] f06da5b26e ui: bump eslint from 8.54.0 to 8.55.0 (#250)
Bumps [eslint](https://github.com/eslint/eslint) from 8.54.0 to 8.55.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.54.0...v8.55.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-07 20:48:14 +00:00
dependabot[bot] e00bfa0187 ui: bump vite from 4.5.0 to 4.5.1 (#251)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.1/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-07 20:45:08 +00:00
dependabot[bot] 4a008e4c2f ui: bump @types/papaparse from 5.3.11 to 5.3.14 (#249)
Bumps [@types/papaparse](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/papaparse) from 5.3.11 to 5.3.14.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/papaparse)

---
updated-dependencies:
- dependency-name: "@types/papaparse"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-07 20:35:37 +00:00
Mr 8c84fe29f9 Merge pull request #254 from sbondCo/fix-tooltip-overflow
Fix tooltip and nav overflow
2023-12-07 00:10:13 +00:00
IRHM 3bd03e4e97 nav: fix overflow and improve responsiveness 2023-12-07 00:08:14 +00:00
IRHM 4d65a16584 make tooltips fixed position
to stop overflowing when page resizes
2023-12-06 20:24:56 +00:00
Mr 21b4e49aa0 Merge pull request #253 from sbondCo/sort-indicators
Basic sort and filter indicators
2023-12-06 20:03:45 +00:00
IRHM 1bcc521d3a Basic sort and filter indicators 2023-12-06 20:01:49 +00:00
IRHM 9fa85e3191 v1.27.0 2023-12-03 23:49:12 +00:00
IRHM 55ff90a190 1.27.0-dev1 2023-12-03 23:38:51 +00:00
IRHM 2bf7639ecf fix user cards on dark theme 2023-12-03 23:38:06 +00:00
IRHM 2aabd94311 1.27.0-dev 2023-12-03 23:25:07 +00:00
Mr 17a61c0248 Posters - Use cached images on watched list (#243)
* upload avatar: add comment

* Poster: Use local cached image if show is on watched list
2023-12-03 15:41:12 +00:00
IRHM a17c7a4ce5 profile: update user state when bio updates 2023-12-03 15:28:45 +00:00
IRHM f31009fa5e list: fix profile data not updating 2023-12-03 15:28:45 +00:00
IRHM 559ac8945a fix name overflows not showing ellipsis 2023-12-03 15:28:45 +00:00
IRHM 1ce7f5a938 list: center user card
think it looks better like this
2023-12-03 15:28:45 +00:00
IRHM d634956d76 list: hide bio if not set and add gap to user-row 2023-12-03 15:28:45 +00:00
IRHM 3275ad6620 list: add title of full bio 2023-12-03 15:28:45 +00:00
IRHM 3ac9e94f89 list: fix bio and clamp to 2 lines
i dont care about the newlines anymore
2023-12-03 15:28:45 +00:00
IRHM 9562b3fef0 profile: fix and add notifs to post requests 2023-12-03 15:28:45 +00:00
IRHM ae95329d06 Show avatar/bio on user page, change follow btn to an icon 2023-12-03 15:28:45 +00:00
IRHM 1e81872876 move profile avatar to new UserAvatar component 2023-12-03 15:28:45 +00:00
IRHM 90eb422c9d fix user search and display proper errors 2023-12-03 15:28:45 +00:00
IRHM 3bfcb741f1 tasks: setup daily tasks interval 2023-12-03 15:28:45 +00:00
IRHM e04d72e565 images: cleanup: change debug to info logs 2023-12-03 15:28:45 +00:00
IRHM 99aa4e564a support bio 2023-12-03 15:28:45 +00:00
IRHM eac5c5a91e images: update comment 2023-12-03 15:28:45 +00:00
IRHM 82e9a4c0ea check file type is supported image 2023-12-03 15:28:45 +00:00
IRHM 4dd494dc80 profile: update user avatar when changed 2023-12-03 15:28:45 +00:00
IRHM 4422e83cd7 wip cleanup images task 2023-12-03 15:28:45 +00:00
IRHM eec6e00dac profile: remove canvas after image load
and remove z-indexes on img and canvas
2023-12-03 15:28:45 +00:00
IRHM 579736eb10 perform blurhash magic
probs wanna remove that canvas after img loads
2023-12-03 15:28:45 +00:00
IRHM 06fadeffa1 profile: show avatar 2023-12-03 15:28:45 +00:00
IRHM 97c87ecbf0 wip setting user avatar
uploading and setting working, need to display
2023-12-03 15:28:45 +00:00
Mr e55593d62b Nav: Hide buttons when searchbar is focused (#241) 2023-11-29 16:56:15 +00:00
dependabot[bot] 2a3b5c8746 server: bump github.com/gin-contrib/cors from 1.4.0 to 1.5.0 in /server (#232)
Bumps [github.com/gin-contrib/cors](https://github.com/gin-contrib/cors) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/gin-contrib/cors/releases)
- [Changelog](https://github.com/gin-contrib/cors/blob/master/.goreleaser.yaml)
- [Commits](https://github.com/gin-contrib/cors/compare/v1.4.0...v1.5.0)

---
updated-dependencies:
- dependency-name: github.com/gin-contrib/cors
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-29 14:04:04 +00:00
dependabot[bot] 52097df861 ui: bump prettier from 3.0.3 to 3.1.0 (#231)
Bumps [prettier](https://github.com/prettier/prettier) from 3.0.3 to 3.1.0.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.0.3...3.1.0)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-29 14:03:19 +00:00
dependabot[bot] 92637aef3d ui: bump axios from 1.6.1 to 1.6.2 (#230)
Bumps [axios](https://github.com/axios/axios) from 1.6.1 to 1.6.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.6.1...v1.6.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-29 14:02:53 +00:00
dependabot[bot] 02f537c5bf ui: bump svelte-check from 3.6.0 to 3.6.2 (#229)
Bumps [svelte-check](https://github.com/sveltejs/language-tools) from 3.6.0 to 3.6.2.
- [Release notes](https://github.com/sveltejs/language-tools/releases)
- [Commits](https://github.com/sveltejs/language-tools/compare/svelte-check-3.6.0...svelte-check-3.6.2)

---
updated-dependencies:
- dependency-name: svelte-check
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-29 14:02:08 +00:00
dependabot[bot] c63ab3cddf server: bump golang.org/x/crypto from 0.15.0 to 0.16.0 in /server (#233)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.15.0 to 0.16.0.
- [Commits](https://github.com/golang/crypto/compare/v0.15.0...v0.16.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-29 14:01:33 +00:00
dependabot[bot] cf7457a671 ui: bump @typescript-eslint/parser from 6.11.0 to 6.13.1 (#240)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.11.0 to 6.13.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.13.1/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-29 14:00:19 +00:00
dependabot[bot] 095cc21ea4 ui: bump @typescript-eslint/eslint-plugin from 6.11.0 to 6.13.0 (#227)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.11.0 to 6.13.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.13.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-29 13:57:56 +00:00
IRHM 983f4efa95 v1.26.0 2023-11-28 02:53:50 +00:00
IRHM 4573d69bbf 1.26.0-dev2 2023-11-28 02:45:19 +00:00
IRHM 07cbcb4d41 fix search page padding/margin 2023-11-28 02:44:44 +00:00
IRHM 71e31df2bf 1.26.0-dev1 2023-11-28 02:27:26 +00:00
IRHM f969c591c1 nav menus: fix themeing on arrow 2023-11-28 02:26:50 +00:00
IRHM 6d1e95ff5c 1.26.0-dev 2023-11-28 02:19:02 +00:00
Mr e7a6a888b1 watched: changed movie default status to FINISHED (#239)
* watched: changed movie default status to FINISHED

when adding content to list by rating instead of giving status.

* update comment
2023-11-28 02:16:19 +00:00
Mr ab9fdddc14 Merge pull request #234 from sbondCo/following
Following Users & Fix user lists posters
2023-11-28 02:02:52 +00:00
IRHM 6160e906df poster: fix interaction-disabled for user lists 2023-11-28 01:49:18 +00:00
IRHM dafac30df1 hide following tooltip if menu open 2023-11-26 19:32:45 +00:00
IRHM e98b6a383d style user page 2023-11-26 19:30:50 +00:00
IRHM 679168d88c unfollow, responsive height sub menus 2023-11-26 19:11:11 +00:00
IRHM 8679cdf0c6 tsconfig: include env.d.ts 2023-11-26 15:06:27 +00:00
IRHM 27e2e98ca3 move sort menu and filter menu to their own components 2023-11-26 13:40:28 +00:00
IRHM 3f90b2f0ca sub menu arrows to icon 2023-11-26 13:28:38 +00:00
IRHM dfa4e81dc2 basic following sub menu 2023-11-26 02:33:14 +00:00
IRHM 3d3bfa50bb wip get followers and add new following icon
also fix compass icon (not filled) and fix tooltips
2023-11-25 21:47:39 +00:00
IRHM f4a4b7c6d7 improve search resposiveness for large screens 2023-11-25 21:21:28 +00:00
IRHM db1b1bee65 wip follow routes 2023-11-25 14:32:19 +00:00
IRHM 53aa76f009 translate errors with gorm
and instead of using strings.Contains, we can compare to gorms error now
2023-11-25 14:31:50 +00:00
IRHM 24fe7f4984 create follows table 2023-11-25 13:31:39 +00:00
IRHM 99d9d4866a Cleanup search page and userslist
by cleanup i mean make look nicer
2023-11-21 20:43:50 +00:00
IRHM 7b0594a39b v1.25.0 2023-11-20 22:39:01 +00:00
IRHM 628dfef8d1 1.25.0-dev 2023-11-20 22:28:21 +00:00
Mr a3e305478b Merge pull request #219 from sbondCo/dependabot/npm_and_yarn/svelte-4.2.7
ui: bump svelte from 4.2.3 to 4.2.7
2023-11-20 22:22:20 +00:00
dependabot[bot] 0ca133f002 ui: bump svelte from 4.2.3 to 4.2.7
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 4.2.3 to 4.2.7.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/svelte@4.2.7/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@4.2.7/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-20 22:21:30 +00:00
Mr 0586e91b31 Merge pull request #218 from sbondCo/dependabot/npm_and_yarn/svelte-check-3.6.0
ui: bump svelte-check from 3.5.2 to 3.6.0
2023-11-20 22:20:53 +00:00
Mr 3dbcc7b69f Merge pull request #220 from sbondCo/dependabot/npm_and_yarn/typescript-5.3.2
ui: bump typescript from 5.2.2 to 5.3.2
2023-11-20 22:19:33 +00:00
dependabot[bot] 784b6f2559 ui: bump typescript from 5.2.2 to 5.3.2
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 5.2.2 to 5.3.2.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v5.2.2...v5.3.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-20 22:18:57 +00:00
Mr d61711ace3 Merge pull request #221 from sbondCo/dependabot/npm_and_yarn/eslint-8.54.0
ui: bump eslint from 8.53.0 to 8.54.0
2023-11-20 22:18:41 +00:00
Mr c520cd6be0 Merge pull request #222 from sbondCo/dependabot/npm_and_yarn/sveltejs/kit-1.27.6
ui: bump @sveltejs/kit from 1.27.5 to 1.27.6
2023-11-20 22:18:14 +00:00
Mr 00a3b7041e Merge pull request #223 from sbondCo/tmdb-ratings
Import rating and rating date from TMDb, give activity customDate column
2023-11-20 22:17:25 +00:00
IRHM 6a69fbdab2 i forgot this is a pointer 2023-11-20 22:14:53 +00:00
IRHM deb30ebf03 import: only add IMPORTED_RATING activity if we have a customDate to use
otherwise this activity is useless and a duplicate of the normal IMPORTED activity
2023-11-20 22:13:08 +00:00
IRHM 985e5b3d39 Change imported rating activity text when there is no rating
to "added to watchlist with no rating"
2023-11-20 22:11:02 +00:00
IRHM 2b3dcd57d3 Show activity with customDate if set
also setup imported rating text - same as normal rating, but distinct so we have the ability to change it later if we want
2023-11-20 21:57:40 +00:00
dependabot[bot] 4200b03736 ui: bump @sveltejs/kit from 1.27.5 to 1.27.6
Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 1.27.5 to 1.27.6.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/master/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@1.27.6/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-20 19:57:56 +00:00
dependabot[bot] 7190a43604 ui: bump eslint from 8.53.0 to 8.54.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.53.0 to 8.54.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.53.0...v8.54.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-20 19:57:43 +00:00
dependabot[bot] 7c77d9c536 ui: bump svelte-check from 3.5.2 to 3.6.0
Bumps [svelte-check](https://github.com/sveltejs/language-tools) from 3.5.2 to 3.6.0.
- [Release notes](https://github.com/sveltejs/language-tools/releases)
- [Commits](https://github.com/sveltejs/language-tools/compare/svelte-check-3.5.2...svelte-check-3.6.0)

---
updated-dependencies:
- dependency-name: svelte-check
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-20 19:57:10 +00:00
IRHM bd844b17c4 Support CustomDate on activity and add import rating activity using it 2023-11-20 15:33:02 +00:00
IRHM 9bf3e6728c Support importing "Your Rating" from tmdb export 2023-11-20 14:36:50 +00:00
IRHM 78542120da v1.24.0 2023-11-19 20:55:31 +00:00
IRHM 6148773965 1.24.0-dev 2023-11-19 20:34:24 +00:00
Mr 1796bc9fb7 Merge pull request #216 from sbondCo/db-importing
TMDb Importing
2023-11-19 20:21:09 +00:00
IRHM 86c82181e1 add try again to err message 2023-11-19 20:17:19 +00:00
IRHM 51aec112b4 add error notify when no files found in dropped items
Co-Authored-By: Mathieu Moalic <21007252+mathieumoalic@users.noreply.github.com>
2023-11-19 20:14:22 +00:00
IRHM bffb7960cc import: update not supported text
Co-Authored-By: Mathieu Moalic <21007252+mathieumoalic@users.noreply.github.com>
2023-11-19 20:09:58 +00:00
IRHM 075b688605 basic tmdb import support
Co-Authored-By: Mathieu Moalic <21007252+mathieumoalic@users.noreply.github.com>
2023-11-19 19:45:46 +00:00
IRHM 6f10d221cf v1.23.0 2023-11-19 06:51:49 +00:00
IRHM 37fad5b8d3 v1.23.0 2023-11-19 06:47:53 +00:00
IRHM 8c544535c6 1.23.0-dev 2023-11-19 06:38:55 +00:00
Mr 23d407fbd6 Merge pull request #213 from sbondCo/admin-stats
Admin stats
2023-11-19 02:09:11 +00:00
IRHM 20ca9ff2be Make most watched stats take you to content on click 2023-11-19 02:05:21 +00:00
IRHM 1b8a8ba491 routes: use store somewhere else so it was worth moving in last commit
told u am smart
2023-11-19 01:50:43 +00:00
IRHM e7d93649c2 routes: move in memory store var to BaseRouter
for REUSE becuz im smart
2023-11-19 01:48:08 +00:00
IRHM 53b8b1d500 getServerStats: Add error logging even though my code has 0% chance of errorring 2023-11-19 01:37:53 +00:00
IRHM 6ae214bd98 Stats: Remove unused media query 2023-11-19 01:20:10 +00:00
IRHM 7f067c62a4 get and display server stats 2023-11-19 01:19:39 +00:00
IRHM 2e6bfc9985 move profile stats to reusable components 2023-11-18 23:16:09 +00:00
IRHM 33ea4cd3fe setup server stats route and service 2023-11-18 23:15:51 +00:00
IRHM 6211901602 v1.22.0 2023-11-17 21:07:17 +00:00
IRHM 4a50de48f0 Dockerfile: Copy in arr package 2023-11-16 00:58:29 +00:00
IRHM b58b92b00f 1.22.0-dev 2023-11-15 23:51:19 +00:00
IRHM 427c70ccb3 getEnabledFeatures: Only get real data for admins
Currently only supports sonarr/radarr, but we dont have perms that for that yet. When we do we can check for that perm instead.
2023-11-15 23:50:15 +00:00
IRHM 8d0af94f9a add beta notice to services 2023-11-15 23:37:40 +00:00
IRHM bbaa830fd9 fix selecting an arr server to use 2023-11-15 23:31:05 +00:00
Mr 221de60c11 Merge pull request #207 from sbondCo/dependabot/go_modules/server/github.com/golang-jwt/jwt/v5-5.1.0
server: bump github.com/golang-jwt/jwt/v5 from 5.0.0 to 5.1.0 in /server
2023-11-15 21:14:08 +00:00
dependabot[bot] c0ff48dc1f server: bump github.com/golang-jwt/jwt/v5 from 5.0.0 to 5.1.0 in /server
Bumps [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) from 5.0.0 to 5.1.0.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v5.0.0...v5.1.0)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v5
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-15 21:13:29 +00:00
Mr ea4dade1f7 Merge pull request #206 from sbondCo/dependabot/go_modules/server/golang.org/x/crypto-0.15.0
server: bump golang.org/x/crypto from 0.14.0 to 0.15.0 in /server
2023-11-15 21:12:41 +00:00
Mr 6432b18beb Merge pull request #205 from sbondCo/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-6.11.0
ui: bump @typescript-eslint/eslint-plugin from 6.10.0 to 6.11.0
2023-11-15 21:11:47 +00:00
dependabot[bot] 16b4b87185 ui: bump @typescript-eslint/eslint-plugin from 6.10.0 to 6.11.0
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.10.0 to 6.11.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.11.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-15 21:11:16 +00:00
Mr 126db58cc4 Merge pull request #204 from sbondCo/dependabot/npm_and_yarn/svelte-4.2.3
ui: bump svelte from 4.2.2 to 4.2.3
2023-11-15 21:10:57 +00:00
dependabot[bot] 67d62ef957 ui: bump svelte from 4.2.2 to 4.2.3
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 4.2.2 to 4.2.3.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG-pre-5.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@4.2.3/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-15 21:10:22 +00:00
Mr 3e1607e09d Merge pull request #203 from sbondCo/dependabot/npm_and_yarn/typescript-eslint/parser-6.11.0
ui: bump @typescript-eslint/parser from 6.10.0 to 6.11.0
2023-11-15 21:09:58 +00:00
Mr cbbc4de5a0 Merge pull request #202 from sbondCo/dependabot/npm_and_yarn/axios-1.6.1
ui: bump axios from 1.6.0 to 1.6.1
2023-11-15 21:09:32 +00:00
Mr b4f16f12e3 Merge pull request #201 from sbondCo/dependabot/npm_and_yarn/svelte-preprocess-5.1.0
ui: bump svelte-preprocess from 5.0.4 to 5.1.0
2023-11-15 21:09:14 +00:00
Mr 56bf80de7e Merge pull request #210 from sbondCo/hide-req
Hide request button when sonarr/radarr disabled
2023-11-15 21:08:35 +00:00
IRHM e2e791570e get server features again after closing sonarr/radarr modal 2023-11-15 21:06:17 +00:00
IRHM d65f1891bb RadarrModal: Update setting descriptions to be about movies not shows 2023-11-15 20:59:29 +00:00
IRHM 35b1ea951b Create features route - use to only show request btn if feature is enabled 2023-11-15 20:52:47 +00:00
IRHM d76dfbec95 SimilarContent: Only show list if there are any results 2023-11-15 20:15:55 +00:00
IRHM 095015329a fix no server found typo 2023-11-14 19:09:45 +00:00
Mr c7c1760a96 Merge pull request #208 from sbondCo/navts
Fix left positioned tooltips and add tooltips to nav icons
2023-11-14 19:06:39 +00:00
IRHM 74de1f3039 fix left tooltips 2023-11-14 19:05:23 +00:00
IRHM 0ba1cc35c5 add tooltips to nav buttons 2023-11-14 19:00:29 +00:00
dependabot[bot] cb09d71d3d server: bump golang.org/x/crypto from 0.14.0 to 0.15.0 in /server
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.14.0 to 0.15.0.
- [Commits](https://github.com/golang/crypto/compare/v0.14.0...v0.15.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 19:47:58 +00:00
dependabot[bot] 62bd6c8353 ui: bump @typescript-eslint/parser from 6.10.0 to 6.11.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.10.0 to 6.11.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.11.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 19:37:14 +00:00
dependabot[bot] 75cf586407 ui: bump axios from 1.6.0 to 1.6.1
Bumps [axios](https://github.com/axios/axios) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.6.0...v1.6.1)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 19:36:57 +00:00
dependabot[bot] 25f747b6bb ui: bump svelte-preprocess from 5.0.4 to 5.1.0
Bumps [svelte-preprocess](https://github.com/sveltejs/svelte-preprocess) from 5.0.4 to 5.1.0.
- [Changelog](https://github.com/sveltejs/svelte-preprocess/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte-preprocess/commits/v5.1.0)

---
updated-dependencies:
- dependency-name: svelte-preprocess
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 19:36:51 +00:00
Mr d3fdd3cc1e Merge pull request #198 from sbondCo/radarr-reqs
Radarr requesting
2023-11-12 20:27:13 +00:00
IRHM 9022654bdb requesting: use rootfolder defined in server settings 2023-11-12 20:25:18 +00:00
IRHM 71daab3e6c movie: add missing import 2023-11-12 20:16:58 +00:00
IRHM f4b87bdc2a requesting: use actual server name, add content name and year 2023-11-12 20:15:18 +00:00
IRHM 9e7981e80b radarr requesting 2023-11-12 20:09:55 +00:00
IRHM d0a86c4c85 add radarr settings 2023-11-12 13:55:23 +00:00
Mr 06283ca0e4 Merge pull request #197 from sbondCo/dependabot/npm_and_yarn/sveltejs/kit-1.27.5
ui: bump @sveltejs/kit from 1.27.2 to 1.27.5
2023-11-12 02:49:16 +00:00
Mr 2336cc3680 Merge pull request #190 from sbondCo/dependabot/npm_and_yarn/eslint-plugin-svelte-2.35.0
ui: bump eslint-plugin-svelte from 2.34.0 to 2.35.0
2023-11-12 02:48:48 +00:00
dependabot[bot] 312c192ccb ui: bump @sveltejs/kit from 1.27.2 to 1.27.5
Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 1.27.2 to 1.27.5.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/master/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@1.27.5/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-12 02:48:00 +00:00
Mr dbc0c015ef Merge pull request #189 from sbondCo/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-6.10.0
ui: bump @typescript-eslint/eslint-plugin from 6.9.1 to 6.10.0
2023-11-12 02:47:24 +00:00
dependabot[bot] 5b00825b18 ui: bump @typescript-eslint/eslint-plugin from 6.9.1 to 6.10.0
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.9.1 to 6.10.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.10.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-12 02:46:54 +00:00
dependabot[bot] 2a2b6119f7 ui: bump eslint-plugin-svelte from 2.34.0 to 2.35.0
Bumps [eslint-plugin-svelte](https://github.com/sveltejs/eslint-plugin-svelte) from 2.34.0 to 2.35.0.
- [Release notes](https://github.com/sveltejs/eslint-plugin-svelte/releases)
- [Changelog](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/eslint-plugin-svelte/compare/v2.34.0...v2.35.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-svelte
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-12 02:46:50 +00:00
Mr fbd3215be3 Merge pull request #187 from sbondCo/dependabot/npm_and_yarn/typescript-eslint/parser-6.10.0
ui: bump @typescript-eslint/parser from 6.9.1 to 6.10.0
2023-11-12 02:46:06 +00:00
dependabot[bot] b26e59664f ui: bump @typescript-eslint/parser from 6.9.1 to 6.10.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.9.1 to 6.10.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.10.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-12 02:33:00 +00:00
Mr 0a035c2c23 Merge pull request #186 from sbondCo/dependabot/npm_and_yarn/eslint-8.53.0
ui: bump eslint from 8.52.0 to 8.53.0
2023-11-12 02:32:17 +00:00
Mr 9dc63f4ed8 Merge pull request #196 from sbondCo/darktheme-poster-btn
Fix poster button colors in dark theme
2023-11-12 02:31:23 +00:00
IRHM bb8cfe1b52 PosterRating: Make on hover text readable in dark theme 2023-11-12 02:29:13 +00:00
IRHM 8bdc459bc0 fix dark theme poster status no-icon color 2023-11-12 02:20:30 +00:00
Mr 5c37f29382 Merge pull request #195 from sbondCo/allrr2
Sonarr Requesting
2023-11-12 02:08:42 +00:00
IRHM aacc4b1285 fix zindexes 2023-11-11 22:41:58 +00:00
IRHM a3a037775f RequestModal notification 2023-11-11 22:39:56 +00:00
IRHM 868ddfc510 hookup AutomaticSearch setting 2023-11-11 22:31:30 +00:00
IRHM 12eeeb91a2 sonarr season requesting 2023-11-11 22:26:17 +00:00
Mr e16cbfc49e Merge pull request #191 from sbondCo/updated-screens
readme: update and add new screenshots
2023-11-09 21:08:10 +00:00
IRHM d38d7dfd56 update and add new screenshots 2023-11-09 02:05:02 +00:00
IRHM 0fc895b667 create basic seasons list 2023-11-07 21:41:34 +00:00
IRHM 24b986572a cleanup requestcontent
a bit
2023-11-07 18:32:15 +00:00
dependabot[bot] a3aa9adc0e ui: bump eslint from 8.52.0 to 8.53.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.52.0 to 8.53.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.52.0...v8.53.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-06 19:21:42 +00:00
IRHM 3c6b6ae1a5 sonarr basic content requesting working 2023-11-05 17:08:17 +00:00
IRHM c8f8869c28 arr: reload data on modal exit 2023-11-03 03:01:30 +00:00
IRHM 53895462a5 arr: support removing a server 2023-11-03 03:01:30 +00:00
IRHM 6ce3099396 settings: add padding to bottom of page 2023-11-03 03:01:30 +00:00
IRHM 068e6a1684 arr: fix editing and add autoSearch checkbox 2023-11-03 03:01:30 +00:00
IRHM e5052e265d arr: editing server 2023-11-03 03:01:30 +00:00
IRHM 79b4fe1d95 arr: support adding new sonarr server 2023-11-03 03:01:30 +00:00
IRHM 328bbedcf0 arrModal: Show errors of save 2023-11-03 03:01:30 +00:00
IRHM 10285bffd7 Modal: Scroll when not enough space, show above nav 2023-11-03 03:01:30 +00:00
IRHM d7b1b4cf46 redo arr implementation
decided it would be harder to add multiple server support down the road than to just do it right the first time.

modal wont autosave. One endpoint to get all details needed for modal (/test).
2023-11-03 03:01:30 +00:00
IRHM 7b0045fd6c ui: profile: Use Setting component
not using all because it will break the theme setting and i dont wanna have to fix that :()
2023-11-03 03:01:30 +00:00
IRHM 0554ba2667 ui: Move settings to their own components 2023-11-03 03:01:30 +00:00
IRHM a06ad2fd3f ui: Move sonarr settings to modal and fetch quality profiles 2023-11-03 03:01:30 +00:00
IRHM 8d69043779 server: Create GetSafe on ServerConfig struct
to get safe version of config.. still manual for now until i figure out a better way (since we cant us struct tag since we do need the other properties for file)
2023-11-03 03:01:30 +00:00
IRHM 7634f80270 server: Basic flow for using radarr/sonarr setup
New `arr` package created, works how I wanted so far, want to try not have the routes be so duplicated... becuz bad
2023-11-03 03:01:30 +00:00
IRHM 6caed2fd36 config: Create sonarr/radarr settings 2023-11-03 03:01:30 +00:00
Mr ae3518cdc5 README: Update setup section to link to docs 2023-11-02 21:41:17 +00:00
IRHM d5349322ca v1.21.1 2023-11-02 00:41:39 +00:00
Mr 837db05e6e Merge pull request #185 from sbondCo/docker-user
Support changing user to run docker container with & move to alpine based images
2023-11-02 00:37:18 +00:00
IRHM d1f2255f65 workflows: Remove workflow_dispatch trigger from container-release.yml 2023-11-02 00:22:07 +00:00
IRHM fab012ae09 move to alpine base images
image is half of original size now too very nice - 500MB -> 1.2GB -> 200MB
2023-11-02 00:18:40 +00:00
IRHM a3559381bd omit dev dependencies in runner step 2023-11-01 23:48:23 +00:00
IRHM 21a2dc1bf4 1.21.1-dev 2023-11-01 22:21:02 +00:00
IRHM 6a461af848 use node:20 as base image for runner step
updated ui builder step to node:20 too
2023-11-01 22:20:05 +00:00
IRHM aed92177f3 v1.21.0 2023-11-01 04:15:47 +00:00
IRHM a52463259a 1.21.0-dev2 2023-11-01 03:56:32 +00:00
IRHM 4590c6bd56 Add SEASON_REMOVED activity 2023-11-01 03:55:50 +00:00
IRHM efb442b224 1.21.0-dev1 2023-11-01 03:22:13 +00:00
IRHM dd364aca9a SeasonsList: Fix episodes spoiler blur being cut off 2023-11-01 03:21:41 +00:00
IRHM 768d3add59 SeasonsList: Increase bottom margin on topbar 2023-11-01 03:20:05 +00:00
IRHM a18a5ee23a SeasonsList: Fix top on mobile 2023-11-01 03:17:57 +00:00
IRHM 71f294552d 1.21.0-dev 2023-11-01 02:14:23 +00:00
Mr 5d268b93be Merge pull request #183 from sbondCo/seasonslist-styl
SeasonsList: Make season buttons wrap
2023-11-01 02:12:23 +00:00
IRHM 856444bd9f SeasonsList: Make season buttons wrap
Seasons with names will take up less vertical space and look better generally.
2023-11-01 02:08:39 +00:00
Mr 14e3277e6d Merge pull request #176 from sbondCo/dependabot/npm_and_yarn/sveltejs/kit-1.27.2
ui: bump @sveltejs/kit from 1.25.2 to 1.27.2
2023-11-01 01:33:48 +00:00
dependabot[bot] 89aa4b477f ui: bump @sveltejs/kit from 1.25.2 to 1.27.2
Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 1.25.2 to 1.27.2.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/master/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@1.27.2/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-01 01:33:00 +00:00
Mr f4b22c76d6 Merge pull request #175 from sbondCo/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-6.9.1
ui: bump @typescript-eslint/eslint-plugin from 6.9.0 to 6.9.1
2023-11-01 01:32:23 +00:00
Mr 83472ae329 Merge pull request #161 from sbondCo/dependabot/github_actions/actions/setup-node-4
workflow: bump actions/setup-node from 3 to 4
2023-11-01 01:27:47 +00:00
dependabot[bot] 345a465f0f ui: bump @typescript-eslint/eslint-plugin from 6.9.0 to 6.9.1
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.9.0 to 6.9.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.9.1/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-01 01:27:08 +00:00
Mr 7e7a9c886d Merge pull request #177 from sbondCo/dependabot/npm_and_yarn/axios-1.6.0
ui: bump axios from 1.5.1 to 1.6.0
2023-11-01 01:26:51 +00:00
Mr 2abde13fb3 Merge pull request #178 from sbondCo/dependabot/npm_and_yarn/typescript-eslint/parser-6.9.1
ui: bump @typescript-eslint/parser from 6.9.0 to 6.9.1
2023-11-01 01:26:25 +00:00
Mr 57d8de9b54 Merge pull request #182 from sbondCo/seasonlist-stickiness-nav-gone-not-seen-bad-fix-this-now-pls-mr-me
Fix SeasonList being hidden below nav
2023-11-01 00:59:53 +00:00
IRHM 02ae13d8f8 SeasonsList: Make list smaller when nav shown 2023-11-01 00:50:10 +00:00
IRHM 3641105ce2 fix nav hiding seasonlist items 2023-11-01 00:45:00 +00:00
Mr 4dfd84d807 Merge pull request #180 from sbondCo/reveal-show
Add shadow to `Click To Reveal`
2023-10-31 04:18:10 +00:00
IRHM 088fda01a7 SeasonsListEpisode: Use var for shadows 2023-10-31 04:16:53 +00:00
IRHM d827ef7d61 SeasonsListEpisode: Add shadow to click to reveal message 2023-10-31 04:10:22 +00:00
Mr d117416367 Merge pull request #179 from sbondCo/watch-seasons
Watch seasons
2023-10-31 02:12:05 +00:00
IRHM 4c3ce990c9 SeasonsList: Only show status/rating button if show is on watched list 2023-10-31 02:02:51 +00:00
IRHM ba34594c2e ui: support new activity messages 2023-10-31 01:53:39 +00:00
IRHM 5aecf064ba add activity for adding watched items 2023-10-30 22:27:07 +00:00
IRHM ca052df6e0 SeasonsList: De-duplicate updating watched list item 2023-10-30 21:04:04 +00:00
IRHM b95c4852ce support setting watched season rating 2023-10-30 21:01:39 +00:00
IRHM 38ee6253da server: remove testing debug log 2023-10-30 19:48:23 +00:00
dependabot[bot] 0d46917e8d ui: bump @typescript-eslint/parser from 6.9.0 to 6.9.1
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.9.0 to 6.9.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.9.1/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-30 19:26:45 +00:00
dependabot[bot] b406a49c9c ui: bump axios from 1.5.1 to 1.6.0
Bumps [axios](https://github.com/axios/axios) from 1.5.1 to 1.6.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.5.1...v1.6.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-30 19:26:29 +00:00
IRHM 23b02c7e40 support deleting a watched season status 2023-10-30 02:29:09 +00:00
IRHM 0372c8dfd7 Basic update of season status working 2023-10-30 00:58:23 +00:00
IRHM 0197c5eaeb server: Create add/edit watched season endpoint 2023-10-30 00:32:56 +00:00
IRHM 7bd7f98c91 season status outline and make tooltip an arg 2023-10-29 00:43:12 +01:00
IRHM 382c0b4c48 SeasonsList: Create topbar 2023-10-28 21:21:16 +01:00
IRHM 6a46e3b7e2 PosterStatus: Add direction and width props 2023-10-28 21:20:48 +01:00
IRHM 701219b517 tooltip: Add condition opt 2023-10-28 21:20:32 +01:00
IRHM 7e5f1b6ad6 PosterStatus: Different svg padding depending on if status
so we can fit all icons
2023-10-28 20:40:45 +01:00
IRHM d20b1ca0f2 Poster: Move rating and status btns own components 2023-10-28 20:27:20 +01:00
IRHM 852ff0403e ui: Move Poster components to poster folder
gotta organize - gonna be more in a second
2023-10-28 18:38:40 +01:00
IRHM 3d52d96215 server: Create WatchedSeasons table
Each watched season will links to a watched item.
2023-10-28 18:28:17 +01:00
IRHM a3abc549b1 v1.20.1 2023-10-28 04:13:49 +01:00
IRHM 3dfb88bfb4 1.20.1-dev1 2023-10-28 04:02:25 +01:00
Mr 731ebce307 Merge pull request #172 from sbondCo/chrome-issues
Poster: Fix big scrollbars making icons small in sub menus on chromium
2023-10-28 03:01:07 +00:00
IRHM 1eacde4555 Poster: Fix big scrollbars making icons small in sub menus in chromium 2023-10-28 03:01:08 +01:00
IRHM d48e7286d8 1.20.1-dev 2023-10-27 21:48:22 +01:00
Mr 27f081a8c6 Merge pull request #162 from sbondCo/dependabot/npm_and_yarn/typescript-eslint/parser-6.9.0
ui: bump @typescript-eslint/parser from 6.8.0 to 6.9.0
2023-10-27 20:39:21 +00:00
Mr 17d1a33867 Merge pull request #163 from sbondCo/dependabot/npm_and_yarn/vite-4.5.0
ui: bump vite from 4.4.11 to 4.5.0
2023-10-27 20:39:07 +00:00
Mr 8acc07cb94 Merge pull request #164 from sbondCo/dependabot/npm_and_yarn/svelte-4.2.2
ui: bump svelte from 4.2.1 to 4.2.2
2023-10-27 20:38:54 +00:00
dependabot[bot] 7335c83a08 ui: bump svelte from 4.2.1 to 4.2.2
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 4.2.1 to 4.2.2.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/master/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@4.2.2/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-27 20:36:53 +00:00
dependabot[bot] 695aa5bb0b ui: bump @typescript-eslint/parser from 6.8.0 to 6.9.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.8.0 to 6.9.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.9.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-27 20:36:52 +00:00
Mr f7685f22c6 Merge pull request #165 from sbondCo/dependabot/npm_and_yarn/eslint-8.52.0
ui: bump eslint from 8.51.0 to 8.52.0
2023-10-27 20:36:24 +00:00
Mr 952ef27781 Merge pull request #166 from sbondCo/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-6.9.0
ui: bump @typescript-eslint/eslint-plugin from 6.8.0 to 6.9.0
2023-10-27 20:36:07 +00:00
Mr e8f186fd54 Merge pull request #171 from sbondCo/search-issue
Nav: Fix search input not re-focusing after search loads on chromium
2023-10-27 20:34:55 +00:00
IRHM d0a964775e nav: fix search unfocusing on chromium 2023-10-27 21:30:19 +01:00
IRHM 866432d22e Revert "nav: fix searchbar un-focusing on chrome"
This reverts commit 5dce12c73b.
2023-10-27 21:13:51 +01:00
IRHM 5dce12c73b nav: fix searchbar un-focusing on chrome
autofocus fixes this for us now, we won't use it anywhere else.
2023-10-27 21:11:51 +01:00
IRHM 6d002b4d1a nav: add ? operator to classList usage
when chrome doesnt focus input and you press space, it will scroll down, for some reason that will fail too.
2023-10-27 20:10:08 +01:00
dependabot[bot] 5a66b51b00 ui: bump @typescript-eslint/eslint-plugin from 6.8.0 to 6.9.0
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.8.0 to 6.9.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.9.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-23 19:51:58 +00:00
dependabot[bot] 16d53be62c ui: bump eslint from 8.51.0 to 8.52.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.51.0 to 8.52.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.51.0...v8.52.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-23 19:51:24 +00:00
dependabot[bot] afe1947bd3 ui: bump vite from 4.4.11 to 4.5.0
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.4.11 to 4.5.0.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.0/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-23 19:50:54 +00:00
dependabot[bot] 40bd349e22 workflow: bump actions/setup-node from 3 to 4
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-23 19:23:38 +00:00
Mr faff9ff366 Merge pull request #160 from sbondCo/dependabot/npm_and_yarn/typescript-eslint/parser-6.8.0
ui: bump @typescript-eslint/parser from 6.7.5 to 6.8.0
2023-10-16 19:37:23 +00:00
dependabot[bot] e7b0d4f3d1 ui: bump @typescript-eslint/parser from 6.7.5 to 6.8.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.7.5 to 6.8.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.8.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-16 19:36:06 +00:00
Mr a8650a47a5 Merge pull request #159 from sbondCo/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-6.8.0
ui: bump @typescript-eslint/eslint-plugin from 6.7.5 to 6.8.0
2023-10-16 19:35:04 +00:00
dependabot[bot] 1a0319f316 ui: bump @typescript-eslint/eslint-plugin from 6.7.5 to 6.8.0
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.7.5 to 6.8.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.8.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-16 19:33:36 +00:00
IRHM fc943d2eea v1.20.0 2023-10-15 19:29:36 +01:00
IRHM edc292be19 v1.20.0-dev 2023-10-15 19:18:43 +01:00
Mr 01d78695a8 Merge pull request #158 from sbondCo/similar-content
Similar Content Section
2023-10-15 14:26:34 +00:00
IRHM be14a4689c tv: fix PageError error prop 2023-10-15 15:19:21 +01:00
IRHM aea4aab78a Show similar content
had to rework content pages to allow for navigating to another show/movie, since sveltekit wont refresh the whole page when only changing a page parameter
2023-10-15 15:16:12 +01:00
Mr 0619338a97 Merge pull request #157 from sbondCo/fix-cast-text
Content: Fix `cast` header text scrolling out of view
2023-10-15 02:13:06 +00:00
IRHM 41677ea717 SeasonsList: Put max-width on season names
Some shows give seasons names, which when long, would break the episode view.
2023-10-15 02:58:49 +01:00
IRHM 6304c33436 Fix Cast header scrolling out of view
Moved styling for scroll into HorizontalList component.
2023-10-15 02:55:46 +01:00
Mr 41af9c9fe1 Merge pull request #156 from sbondCo/spoilerz
New `Hide Spoilers` Setting
2023-10-15 01:44:41 +00:00
IRHM bade703b4d SeasonsListEpisode: Add transition to blur 2023-10-15 02:40:27 +01:00
IRHM 7220684a97 SeasonsListEpisode: Support hiding spoilers
Hide episode image and name until user reveals
2023-10-15 02:34:13 +01:00
IRHM da7f62af12 SeasonsList: Move episode li out to its own component 2023-10-15 01:54:09 +01:00
IRHM 9054729069 server: Support multiple settings in userUpdate, add spoiler setting
and technically ui but wutevr
2023-10-14 22:29:21 +01:00
Mr 444105955e Merge pull request #155 from sbondCo/dependabot/github_actions/docker/build-push-action-5
workflow: bump docker/build-push-action from 4 to 5
2023-10-14 20:41:32 +00:00
Mr a23bfc5fac Merge pull request #153 from sbondCo/dependabot/github_actions/docker/login-action-3
workflow: bump docker/login-action from 2 to 3
2023-10-14 20:40:57 +00:00
Mr 0934f57fdb Merge pull request #152 from sbondCo/dependabot/github_actions/docker/metadata-action-5
workflow: bump docker/metadata-action from 4 to 5
2023-10-14 20:40:08 +00:00
dependabot[bot] 4669994b4a workflow: bump docker/login-action from 2 to 3
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-14 20:37:06 +00:00
Mr 1135e1132a Merge pull request #154 from sbondCo/dependabot/github_actions/actions/checkout-4
workflow: bump actions/checkout from 3 to 4
2023-10-14 20:36:30 +00:00
dependabot[bot] a22c830357 workflow: bump docker/build-push-action from 4 to 5
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-14 20:29:44 +00:00
dependabot[bot] f09005d171 workflow: bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-14 20:29:41 +00:00
dependabot[bot] 459c729da1 workflow: bump docker/metadata-action from 4 to 5
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 4 to 5.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Upgrade guide](https://github.com/docker/metadata-action/blob/master/UPGRADE.md)
- [Commits](https://github.com/docker/metadata-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-14 20:29:37 +00:00
Mr aacdfebdcf Create dependabot.yml config 2023-10-14 20:29:19 +00:00
IRHM 41ad80c1a9 v1.19.1 2023-10-14 16:22:05 +01:00
IRHM a472b18981 1.19.1-dev 2023-10-14 15:43:38 +01:00
Mr 7d92901f80 Merge pull request #151 from sbondCo/update-deps
Update deps
2023-10-14 14:35:14 +00:00
IRHM 0a83567c11 doc: Update deps 2023-10-14 15:21:37 +01:00
IRHM 007a87b842 ui: update deps 2023-10-14 15:01:41 +01:00
IRHM cbac14f77e server: Update deps 2023-10-14 14:52:50 +01:00
Mr a6fcd93244 Merge pull request #150 from sbondCo/imgbot
[ImgBot] Optimize images
2023-10-13 22:55:56 +00:00
ImgBotApp 2801c5540f [ImgBot] Optimize images
*Total -- 1,748.69kb -> 1,661.01kb (5.01%)

/doc/static/img/social-card.png -- 58.22kb -> 46.23kb (20.59%)
/doc/docs/usage/img/content-full-page.png -- 331.69kb -> 305.20kb (7.99%)
/doc/docs/usage/img/search-for-content-hover-poster.png -- 334.93kb -> 321.19kb (4.1%)
/doc/docs/usage/img/search-for-content-hover-poster-status.png -- 329.38kb -> 316.68kb (3.86%)
/doc/docs/usage/img/search-for-content-hover-poster-rate.png -- 313.64kb -> 302.67kb (3.5%)
/doc/docs/usage/img/search-for-content.png -- 380.83kb -> 369.03kb (3.1%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2023-10-13 02:08:43 +00:00
Mr 20b3a2631a Merge pull request #149 from sbondCo/finish-docs
from source installation and adding to list guides
2023-10-13 00:56:07 +00:00
IRHM 3a7c6c07d5 test-docs workflow: Add docs path scope 2023-10-13 01:54:23 +01:00
IRHM 6b8d686dae docs: Remove docusaurus icons, create watcharr social card 2023-10-13 01:44:15 +01:00
IRHM 83d869417b docs: intro: Link to add to watchlist guide for general usage 2023-10-13 01:13:21 +01:00
IRHM 9bf216537c docs: usage: Create adding to watchlist guide 2023-10-13 01:10:17 +01:00
IRHM 611b2bbd43 docs: Disable dark theme toggle
not setup to support dark theme currently
2023-10-13 00:43:04 +01:00
IRHM 2b35e0b270 docs: installation: Add warnings 2023-10-13 00:38:37 +01:00
IRHM 7e57e50b99 docs: installation/from-source.md: Change tag to <tag> placeholder
Incase people are blindly copy pasting the commands, at least with this, the command will fail, instead of putting them on v1.19.0
2023-10-13 00:34:46 +01:00
IRHM 0703a511c0 docs: installation/from-source.md: Rough steps 2023-10-13 00:09:11 +01:00
IRHM fba76e38fb docs: homepage: Add description padding and center text 2023-10-12 22:35:46 +01:00
IRHM 7960876873 publish-docs: Fix publish_dir
think it is from root instead of the working-directory
2023-10-12 00:40:27 +01:00
IRHM 2395908c20 publish-docs: Add cname 2023-10-12 00:26:06 +01:00
IRHM a3e8877199 docs: no change 2023-10-12 00:16:57 +01:00
Mr bca9336d4f Merge pull request #148 from sbondCo/docs
Create docs
2023-10-11 22:38:39 +00:00
IRHM 4565af412b docs: add workflows 2023-10-11 23:06:58 +01:00
IRHM 232d704d9f docs: Fix homepage get started link 2023-10-11 20:47:44 +01:00
IRHM 869b13f473 docs: Create usage section 2023-10-11 20:46:55 +01:00
IRHM b28e06f559 Create docusaurus project
Change homepage and added docker compose install steps. Still a wip.
2023-10-10 22:55:55 +01:00
IRHM 3af9177849 v1.19.0 2023-10-09 16:28:45 +01:00
Mr f83ec47bc3 Merge pull request #147 from sbondCo/v1.19
v1.19.0 - Updated readme steps & initial config setup fix
2023-10-09 15:27:16 +00:00
IRHM 4cda259a2a remove .env volume from docker-compose files 2023-10-09 16:26:13 +01:00
IRHM 4a6932f5b1 README: Remove .env volume from example compose file 2023-10-09 16:25:48 +01:00
IRHM 0b508772b8 v1.19.0-dev1 2023-10-09 15:59:05 +01:00
IRHM e5dfe96186 config: don't overwrite whole Config object when generating jwt_secret
So that the default options are not overridden (in this case SIGNUP_ENABLED was being reset back to false)
2023-10-09 15:41:06 +01:00
IRHM 1a20e1cdd9 README: Update setup guide to remove .env section 2023-10-09 14:18:37 +01:00
IRHM 6f47c291cb v1.19.0-dev 2023-10-09 14:18:37 +01:00
Mr 79a64fd667 Merge pull request #146 from sbondCo/update-userinfo-after-get-admin
Update userinfo after get admin
2023-10-09 02:30:51 +00:00
IRHM dba92dc3e0 request_admin: Fix instructions typo 2023-10-09 03:29:11 +01:00
IRHM f79393d072 useAdminToken: Update userInfo permissions state on success 2023-10-09 03:26:09 +01:00
Mr 350c6db596 Merge pull request #145 from sbondCo/settings-fix-empty-input
config: Fix panic if `v` unset in `updateConfig`
2023-10-09 02:14:11 +00:00
IRHM b9c0a9f700 config: Fix panic if v unset in updateConfig 2023-10-09 03:12:49 +01:00
Mr 03572c1324 Merge pull request #144 from sbondCo/ui-settings
Web UI Server Settings
2023-10-09 01:21:50 +00:00
IRHM 1eef015e5e config: remove test log 2023-10-09 02:05:20 +01:00
IRHM 00d284bb07 Nav: Close subMenu after clicking settings 2023-10-09 02:05:10 +01:00
IRHM 56b44bd9ab server settings: Add warning to jellyfin host 2023-10-09 01:49:23 +01:00
IRHM 170c96ac8a config: Remove TMDB_KEY variable and make getTMDBKey func 2023-10-09 01:42:10 +01:00
IRHM 416ce62081 server settings: Make jellyfin host editable
editable through web ui with live change.

AvailableAuthProviders moved into /auth/available route and is filled out on each request
2023-10-08 23:25:30 +01:00
IRHM 06bfae19ed Update logging level when changed from ui 2023-10-08 20:19:16 +01:00
IRHM c3b0411bb5 Server Settings: Support updating jellyfin host and tmdb key 2023-10-08 04:30:08 +01:00
IRHM ba3b8d9281 Checkbox: Add name prop
So we can support more than one checkbox on a single page, otherwise they all target the first checkbox
2023-10-08 04:22:02 +01:00
IRHM 1378193031 wip: Server settings updating through webui
- Setup the page
- Setup get/update config routes
2023-10-08 04:16:18 +01:00
IRHM 59a705222f Create user info endpoint
and use it to store user info in a writable store (instead of using localStorage). This will let us easily add more info in the future without requiring the user to login again.
2023-10-07 21:19:33 +01:00
Mr 1e50a4fa5b Merge pull request #142 from sbondCo/admin-tokens
Admin tokens
2023-10-05 23:48:54 +00:00
IRHM 2b36272c2d Finalize setupTasks and token cleanup 2023-10-06 00:41:47 +01:00
IRHM e1ead91d2c Create launch.json
autogenerated, for debugging in vscode
2023-10-06 00:31:22 +01:00
IRHM 80e7062151 useAdminToken: Fix getting token from db 2023-10-06 00:30:54 +01:00
IRHM 57d124c31e using tokens 2023-10-05 21:39:08 +01:00
IRHM 9ebfc2d18a token: request button on web ui, display in log and fix creating in db 2023-10-05 19:25:30 +01:00
IRHM a07d9e2882 wip: auth tokens
need to finish and test
2023-10-04 19:50:48 +01:00
Mr e605c8e71a Merge pull request #141 from sbondCo/admin-users
Admin users and json config
2023-10-03 20:14:35 +00:00
IRHM 1387aa3a82 JELLYFIN_HOST: Use config instead of env 2023-10-03 20:52:55 +01:00
IRHM e29f782616 Fix signup_enabled config
Use from config instead of env var and ensure default is `true`.
2023-10-03 20:47:27 +01:00
IRHM da3b932be2 runUI: extra info on each error log so we know where it errors 2023-10-02 23:29:31 +01:00
IRHM 0a649acc0f Delete .env.example 2023-10-02 23:19:17 +01:00
IRHM 4db88d040c Create setup page 2023-10-02 23:13:00 +01:00
IRHM cf6136ce92 auth/available: Return isInSetup
Web ui will be using this soon to notify user that they are creating the first admin account.
2023-10-02 22:10:41 +01:00
IRHM 1acdd245a2 Use Count instead of trying to get first user
to check if we are in setup/first user is being created
2023-10-02 22:09:20 +01:00
IRHM 1c31dd31ac logging: Define logLevel in setupLogging and return it in main for editing
so we dont have it as a global var for no reason
2023-10-02 22:02:37 +01:00
IRHM e9b75abe16 Update logging level depending on config 2023-10-02 21:59:14 +01:00
IRHM b098a25c6a Create permissions and add create_admin setup route 2023-10-02 21:25:53 +01:00
IRHM 9081d3d752 Config: Create readConfig, which runs generate if config file doesnt exist
Also make initFromConfig func (based on old ensureEnv func)
2023-09-30 09:52:46 +01:00
IRHM 383e0bf2f4 Generate barebones json config if not found
generates a JWT_SECRET
2023-09-28 23:22:49 +01:00
IRHM a600affaac v1.18.0 2023-09-25 00:50:10 +01:00
IRHM c436219b62 v1.18.0-dev1 2023-09-25 00:42:39 +01:00
Mr fd726c4cbe Merge pull request #139 from sbondCo/nav-menu-fixes
Nav menu dark theme fixes
2023-09-24 23:42:02 +00:00
IRHM 828f6f3aa3 Nav Face: Fix theming 2023-09-25 00:40:43 +01:00
IRHM 5b14857c8d Nav Icons: Fix theming 2023-09-25 00:39:28 +01:00
IRHM 82acd83395 Nav Menus: Fix button theming 2023-09-25 00:38:46 +01:00
IRHM 93ebc0380f v1.18.0-dev 2023-09-25 00:28:40 +01:00
Mr 5f4efac9ee Merge pull request #135 from sbondCo/fix-content-btns
Content: Fix play on jellyfin btn
2023-09-24 16:48:41 +00:00
IRHM efe697e1e9 Content: Fix play on jellyfin btn
Works on librewolf but not firefox esr.. not sure exactly why
2023-09-24 17:46:38 +01:00
Mr b90458ebb9 Merge pull request #134 from sbondCo/fix-pageload-noauth
Fix import page loading without auth
2023-09-23 20:05:20 +00:00
IRHM bade3f54cb Fix import pages loading without auth
Turns out the goto in import/process will still run after the first goto we run in the layout file if theres no token, so it's possible to end up on /import if you navigate to /import/process first.

added quick fix
2023-09-23 21:04:10 +01:00
Mr 975fddfc6b Merge pull request #133 from sbondCo/filtering
Watched List Filtering
2023-09-23 19:45:39 +00:00
IRHM 31c23ac860 Store: Save activeFilters to localstorage
I'm dumb so it has to go in activeFilterReal key
2023-09-23 20:44:54 +01:00
IRHM 7519098d47 Nav: Change filter btn stroke width on hover 2023-09-23 15:25:10 +01:00
IRHM 4745d528f2 Nav: Manually space btns 2023-09-23 15:23:11 +01:00
IRHM a4d0acb5dd Filters: Manually add each button
so we can change 'hold' text to 'held' without complicating the code
2023-09-23 15:07:35 +01:00
IRHM 10bd2e4bd3 Nav: Add menu class to face menu div 2023-09-23 15:03:37 +01:00
IRHM a0f6991746 Filters: Get basic filtering working 2023-09-23 14:49:39 +01:00
IRHM 61cdf91382 Nav: basic filter menu 2023-09-23 00:49:13 +01:00
IRHM 7f15f796f1 v1.17.0 2023-09-19 21:59:26 +01:00
IRHM 31ace217be v1.17.0-dev2 2023-09-19 20:47:56 +01:00
IRHM c502950473 import/process: Scroll to top when start import clicked 2023-09-19 20:46:57 +01:00
IRHM 60e0652123 Modal: Make backdrop fixed 2023-09-19 20:42:47 +01:00
IRHM 53f2ae1bdb v1.17.0-dev1 2023-09-18 22:45:36 +01:00
IRHM 118c6dfba4 Modal: Set position to fixed 2023-09-18 22:44:52 +01:00
IRHM 4d789d46c6 v1.17.0-dev 2023-09-18 22:28:57 +01:00
Mr f461f56299 Merge pull request #132 from sbondCo/import
Importing text watch list
2023-09-18 00:01:59 +00:00
IRHM b16e18fe13 import: Show one file at a time and only text files errors 2023-09-18 00:59:49 +01:00
IRHM 9055bdd58a import: Add beta text to header 2023-09-18 00:50:40 +01:00
IRHM e1e900d7f8 import/process: Disable inputs whilst importing 2023-09-17 17:37:19 +01:00
IRHM 617be082ac import/some-failed: Add some details to what the page is for 2023-09-17 17:23:13 +01:00
IRHM 6738963141 Remove some unused imports 2023-09-17 17:17:02 +01:00
IRHM 64c1f7fc11 Search: Bind search bar value to state
so we can manipulate it.

- When navigating to a search url without searching yourself, we can add the query to the search bar.

- When search page destroys (is navigated away from), we can remove the query from search bar.
2023-09-17 17:16:00 +01:00
IRHM 147561f8c7 import: Create some-failed page for manual processing
So user knows what fails and can attempt to fix it themselves by searching
2023-09-17 16:59:02 +01:00
IRHM 8a5cc8bdef import/process: Remove items that when trimmed are emtpy 2023-09-16 16:26:45 +01:00
IRHM 6fafdf5dc2 import/process: Remove todo note, already done 2023-09-16 15:53:48 +01:00
IRHM a9e871a393 import: Add spinner whilst reading file 2023-09-16 15:53:28 +01:00
IRHM b10c17d485 Fix input colors, trim imported content names 2023-09-16 15:47:54 +01:00
IRHM ccfd5a8fcb v1.16.1 2023-09-15 22:47:55 +01:00
IRHM f6ef3f9fa1 v1.16.1-dev 2023-09-15 22:39:18 +01:00
Mr 690ae67f2a Merge pull request #126 from sbondCo/sticknavdamnitstiiickckckckck
Make Nav Sticky
2023-09-15 21:31:53 +00:00
IRHM d6666a8072 Nav: close submenus when scrolling down 2023-09-15 22:28:31 +01:00
IRHM 9d8140a137 fix nav bg 2023-09-15 22:26:20 +01:00
Mr a701528f7f Merge pull request #131 from stavros-k/patch-1
Do not fail on `.env` file missing
2023-09-15 20:57:47 +00:00
Stavros Kois 87fec68591 Do not fail on .env file missing
In setups where you set the env vars directly, eg Docker or Kubernetes, there is no need to require a .env file
2023-09-12 00:33:07 +03:00
IRHM d425dda240 import/process: Show cancelled importing message 2023-09-09 00:23:04 +01:00
IRHM 12f33bfb3f import/process: Cancel importing on destroy 2023-09-09 00:13:06 +01:00
IRHM cc1df09cb8 import/process: Add padding at bottom of page 2023-09-09 00:02:14 +01:00
IRHM 5f1ef5145a Modal: Make overlay always black
Dont want to flashbang our loyal customers when they open it up
2023-09-08 23:37:05 +01:00
IRHM 405c8169cf import/process: Fix table in dark mode
and add IMPORt_FAILED statement
2023-09-08 23:34:54 +01:00
IRHM 9b2259a9a3 Watched: Add activity as imported when imported 2023-09-08 23:01:24 +01:00
IRHM 9b8fa436b8 Disabled button styling 2023-09-08 23:00:54 +01:00
IRHM c28a1e3793 import: Add item to watched list 2023-09-08 22:43:46 +01:00
IRHM 90dfd39f91 Content: Make content detail request funcs get appentoresponse as param 2023-09-07 18:51:53 +01:00
IRHM 78d14c49b4 import: Add some logging 2023-09-07 18:21:20 +01:00
IRHM 2923337258 import: Filter out people on server 2023-09-07 18:17:42 +01:00
IRHM 7ad6621fbb WatchedList: Add import button on empty watched list 2023-09-07 17:49:56 +01:00
Mr bfab3b2802 Merge pull request #128 from sbondCo/readme-demo-badge-url
README: Update demo badge url
2023-09-06 10:46:40 +00:00
Mr 9283b818f1 README: Update demo badge url
Was still pointing to old domain.
2023-09-06 00:39:43 +00:00
IRHM cc00896fa4 import/process: Add remove row button 2023-09-05 23:05:20 +01:00
IRHM a61f305f03 import/process: Skip empty names and handle NOT_FOUND 2023-09-05 22:29:09 +01:00
IRHM c4b6e41016 Import: Make browse button take up 100% width 2023-09-05 22:28:54 +01:00
IRHM 0e4223787f DropDown: Transparent background in blendIn mode and fix list zindex 2023-09-05 22:18:43 +01:00
IRHM d9abc061ed Modal: Add close button
use it in import/process
2023-09-05 22:14:56 +01:00
IRHM 8f4ea45428 import/process: multi modal: Use year to show item first with that year 2023-09-05 21:45:56 +01:00
IRHM 6366377e9e DropDown: Create blendIn option
and use it for import table type
2023-09-04 17:52:52 +01:00
IRHM 669bfa026d import/process: Allow editing type 2023-09-04 17:46:26 +01:00
IRHM 15c6d902f2 Create DropDown component 2023-09-04 17:44:33 +01:00
IRHM 80f119ca81 import/process: Fix table headers padding 2023-09-04 16:55:19 +01:00
IRHM c287e2fb76 import/process: Create back button 2023-09-04 16:53:34 +01:00
IRHM 4c597508b3 import/process: Show year and type after import 2023-09-04 16:47:21 +01:00
IRHM 42fff9c3cb import: Use equalfold 2023-09-03 20:51:34 +01:00
IRHM c0d6bc0f4d import: Improve multiple results found processing
If we get multiple results, but there is only one result with the name matching 100% then use that as the successful imported item
2023-09-03 20:34:11 +01:00
IRHM 98a5818498 Modal: Use title and desc props 2023-09-03 20:09:30 +01:00
IRHM d228da76c1 import/process: Flow of importing 2023-09-03 19:36:57 +01:00
IRHM 75073e48ad import/process: Show multiple results for imported item 2023-09-03 19:13:32 +01:00
IRHM 3702fa5e36 WIP: import: Setup barebones ep and request to it 2023-09-02 18:50:18 +01:00
IRHM 7bb56331ee import/process: Type number on year inputs 2023-09-02 16:40:45 +01:00
IRHM b953a41485 import/process: Style import button 2023-09-02 15:41:35 +01:00
IRHM a3ead1ca7e import/process: Basic data editing and adding rows 2023-09-02 15:36:59 +01:00
IRHM aeb49bd587 import: Support drag and drop 2023-09-01 20:20:10 +01:00
IRHM c4ac52267b import/process: Return to /import if no list 2023-09-01 20:19:53 +01:00
IRHM b61d8459e0 import/process: Create table and display data from text file 2023-08-31 23:29:08 +01:00
IRHM a12c23a866 import: Read file and basic flow setup with process page 2023-08-31 19:27:26 +01:00
IRHM ebb43b67c3 Setup basic import page 2023-08-31 17:31:09 +01:00
IRHM 080a1f503e Nav: Decent hiding/showing on scroll down/up 2023-08-28 01:14:22 +01:00
IRHM 0b46205f52 Style nav for basic stickyness support 2023-08-28 00:51:48 +01:00
IRHM 79df259714 v1.16.0 2023-08-27 12:48:04 +01:00
IRHM 7b70889118 v1.16.0-dev3 2023-08-27 12:39:57 +01:00
IRHM f5135e0825 Wrap UsersList and fix dark theme accent hover color 2023-08-27 12:38:57 +01:00
IRHM 3fa9dbd109 v1.16.0-dev2 2023-08-27 01:23:38 +01:00
IRHM b2438c735b Poster: Fix dev poster watched color 2023-08-27 01:23:09 +01:00
IRHM f35c42bce5 v1.16.0-dev1 2023-08-27 01:18:39 +01:00
IRHM fd68d7d975 Nav ShareLink: Use origin instead of host, so protocol is included 2023-08-27 01:18:14 +01:00
IRHM 00e45aa6d0 v1.16.0-dev 2023-08-27 01:08:16 +01:00
Mr ce9c92010f Merge pull request #125 from sbondCo/socialize
Socialize - Allow viewing other watchlists
2023-08-27 00:01:18 +00:00
IRHM 0caa18e587 Poster: Fix interactionDisabled btn colors 2023-08-27 01:00:16 +01:00
IRHM db43c313b8 Nav: Don't show share list button if profile is private 2023-08-27 00:53:45 +01:00
IRHM 328f035d1e Nav: Add share list button
Copies your public watchlist link to clipboard
2023-08-27 00:50:31 +01:00
IRHM f1db1ff2b4 Checkbox: Add small delay to reenable
So on fast networks it doesnt look ugly
2023-08-26 23:56:48 +01:00
IRHM 2c6fa24d65 UsersList: Fix long names 2023-08-26 23:46:48 +01:00
IRHM f3b4b7acc4 Profile: Fix long names overflow 2023-08-26 23:39:44 +01:00
IRHM bed4a922c5 Profile: Disable private checkbox whilst request is running 2023-08-26 23:29:49 +01:00
IRHM fc64a66f47 CheckBox: Allow being disabled 2023-08-26 23:29:33 +01:00
IRHM 083381d4d5 Poster: Show 'unrated' text when no rating and remove button style when interactionDisabled 2023-08-26 23:19:14 +01:00
IRHM b4f3bf906b Don't show current user in search results 2023-08-26 23:05:53 +01:00
IRHM 12736f91c9 Show username on others lists 2023-08-26 22:18:07 +01:00
IRHM e1e0ad6db6 Allow viewing other users lists
Moved watchlist page to a component for reuse
2023-08-26 22:12:02 +01:00
IRHM f403e228f6 Search: Display found users 2023-08-26 19:19:22 +01:00
IRHM 242656e2f4 Create user search endpoint 2023-08-26 18:48:34 +01:00
IRHM 62ffee4d84 Create private user setting
.. and user settings support
2023-08-26 18:11:34 +01:00
IRHM b83d567306 Profile: Add private setting 2023-08-26 14:32:11 +01:00
IRHM 9d67db2466 Create Checkbox component 2023-08-26 14:31:49 +01:00
IRHM fb8dec81f1 v1.15.0 2023-08-26 13:30:15 +01:00
IRHM 115ee57687 v1.15.0-dev1 2023-08-26 02:26:26 +01:00
Mr 13c4aff1d3 Merge pull request #123 from sbondCo/fix-content-year
Content: Omit ReleaseDate if not set
2023-08-26 01:25:55 +00:00
IRHM 320a88f1db Content: Omit ReleaseDate if not set 2023-08-26 02:24:27 +01:00
IRHM f175b74320 v1.15.0-dev 2023-08-26 01:43:38 +01:00
Mr 941e8879fd Merge pull request #122 from sbondCo/loading-tooltips
Loading Notifications
2023-08-26 00:37:21 +00:00
IRHM a739862ff1 notify: update type comment 2023-08-26 01:32:59 +01:00
IRHM c240de748b Loading notifs 2023-08-26 01:31:18 +01:00
Mr 80d343e12f Merge pull request #120 from sbondCo/fix-personposter-flash
PersonPoster: Fix white flash on hover
2023-08-24 20:31:07 +00:00
IRHM 3247860cce PersonPoster: Fix white flash on hover
.. by fixing the theme
2023-08-24 21:27:40 +01:00
Mr 67c9edb63d Merge pull request #119 from sbondCo/external-request-caching
TMDB Request Caching
2023-08-24 20:22:01 +00:00
Mr e025d5fbab Merge pull request #118 from sbondCo/imgbot
[ImgBot] Optimize images
2023-08-24 20:07:21 +00:00
Mr fdfda036d4 README: Update dev demo link 2023-08-24 20:03:52 +00:00
ImgBotApp 329fc33c8a [ImgBot] Optimize images
*Total -- 1,629.86kb -> 1,574.90kb (3.37%)

/static/favicon.png -- 17.11kb -> 12.54kb (26.7%)
/screenshot/show-details-page.png -- 303.20kb -> 285.72kb (5.77%)
/static/logo-col.png -- 32.55kb -> 31.38kb (3.57%)
/screenshot/homepage-poster-hover.png -- 431.68kb -> 419.01kb (2.94%)
/screenshot/homepage-poster-change-status.png -- 420.73kb -> 409.36kb (2.7%)
/screenshot/homepage.png -- 424.59kb -> 416.89kb (1.81%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2023-08-22 22:13:05 +00:00
IRHM 9b8b59df5e Add cachePage to rest of content routes 2023-08-22 23:02:20 +01:00
IRHM 5d0bbd9825 wip: Use gorm-contrib/cache middleware
Used on query route for now to test
2023-08-22 22:10:27 +01:00
Mr 4a910c9c57 Merge pull request #111 from kurtmgray/add-year
add year to Poster
2023-08-22 19:51:54 +00:00
IRHM b7ce96c576 Format Poster and Watched page 2023-08-22 18:16:52 +01:00
Kurt Gray 34b756d039 remove data/ 2023-08-22 07:55:50 -07:00
Kurt Gray 3aec67865a Merge branch 'sbondCo:dev' into add-year 2023-08-21 21:29:41 -07:00
Kurt Gray a051a07b1a made requested changes 2023-08-21 21:27:20 -07:00
Kurt Gray 2da1af2f12 requested changes 2023-08-21 21:23:09 -07:00
Kurt Gray bf0b93f90f -dateStr
Co-authored-by: Mr <37304121+IRHM@users.noreply.github.com>
2023-08-21 18:43:27 -07:00
Kurt Gray 76ebdbfe8b conditional on time element
Co-authored-by: Mr <37304121+IRHM@users.noreply.github.com>
2023-08-21 18:42:46 -07:00
IRHM cc94be7d9b v1.14.0 2023-08-21 23:39:37 +01:00
IRHM 11e575faf1 v1.14.0-dev1 2023-08-21 23:00:20 +01:00
IRHM 658c4ba114 Make backdrop mask-image darker 2023-08-21 22:59:52 +01:00
IRHM a62a05790f v1.14.0-dev 2023-08-21 22:32:34 +01:00
Mr 2dbbd08ea3 Merge pull request #116 from sbondCo/byok
Support using custom TMDB_KEY
2023-08-21 21:24:40 +00:00
IRHM 8d4297211a Support using custom TMDB_KEY 2023-08-21 22:22:23 +01:00
Mr 7d8bc81aa9 Merge pull request #115 from sbondCo/slist
SeasonsList: Stop requesting season 1 twice
2023-08-21 21:08:03 +00:00
IRHM 780f922c22 SeasonsList: Stop requesting season 1 twice 2023-08-21 22:07:03 +01:00
Kurt Gray e1420ed4c4 add year to Poster 2023-08-20 16:16:11 -07:00
Mr 89849a0957 Merge pull request #110 from sbondCo/more-sorting-opts
New sorting options: Rating, Last Changed
2023-08-20 20:33:25 +00:00
IRHM 19547861ab Update watched item updatedAt time after update request 2023-08-20 21:28:01 +01:00
IRHM 05739dc085 Add LASTCHANGED and RATING sorting options 2023-08-20 21:05:03 +01:00
Mr d17f24fab9 Merge pull request #109 from sbondCo/discover
Discover
2023-08-20 19:24:12 +00:00
IRHM 075eb01533 Discover: Make sure only movies and tv are shown in trending 2023-08-20 20:23:06 +01:00
IRHM 07f236385f Nav: Fix icon shift when sort menu opened 2023-08-20 20:06:48 +01:00
IRHM 1359279e8c Use small-caps on trending titles 2023-08-20 20:02:35 +01:00
IRHM 577c90402b Discover: Add all trending, upcoming movies and shows 2023-08-20 19:49:25 +01:00
IRHM 9c9436b420 Create working discover page
Reworked Poster, instead of callbacks, handled by the page, Poster just handles the actions itself.

PosterList now supports vertical lists.

Poster can now be `small`
2023-08-20 01:14:29 +01:00
Mr c9a7ec5f66 Merge pull request #106 from sbondCo/backdrop-fade
Fade out content backdrop
2023-08-19 15:20:37 +00:00
IRHM 54c0aca002 Fade out content backdrop
looks nicer i think
2023-08-19 16:20:02 +01:00
Mr 589bd3f86d Merge pull request #105 from sbondCo/empty-list-msg
Show a better empty watched list message
2023-08-19 14:36:39 +00:00
IRHM ffbad648d8 Show a better empty watched list message 2023-08-19 15:34:48 +01:00
Mr c187c7e09d Merge pull request #104 from sbondCo/disable-signup
Support disabling signup
2023-08-19 00:44:38 +00:00
IRHM e57b45e0b8 add signup_enabled env var to examples 2023-08-19 01:43:17 +01:00
IRHM 94c12ac428 Support disabling sign up 2023-08-19 01:19:06 +01:00
IRHM a65604e315 v1.13.0 2023-08-18 22:13:45 +01:00
IRHM 73f6bd2dd5 v1.13.0-dev1 2023-08-18 22:04:52 +01:00
IRHM 993bdeb5f1 ProviderIcon: Fix disney+ logo 2023-08-18 22:03:27 +01:00
IRHM c06609cc2e Dockerfile: Use golang 1.21 2023-08-18 21:37:26 +01:00
IRHM fe2751d9a4 v1.13.0-dev 2023-08-18 21:32:22 +01:00
Mr 873833a381 Merge pull request #101 from sbondCo/jellyfin-provider
Play In Jellyfin / Providers
2023-08-18 20:31:23 +00:00
IRHM 98f5dc5394 auth: force logout old tokens
Changes in this branch require a new login, this is the easiest way to force logout of old tokens................ i think
2023-08-18 21:30:57 +01:00
IRHM 82ec4aa0e7 Only request jf content if user is jellyfin type 2023-08-18 21:16:40 +01:00
IRHM 7ab1d4b7a1 auth: fix jellyfin failed to create user error 2023-08-18 20:43:55 +01:00
IRHM 0f29aeb1d0 content: play on jellyfin button 2023-08-18 17:01:44 +01:00
IRHM c295cc490a New jellyfin endpoint for checking if content exists
Quick test on dev server proves working, needs UI then test on real server
2023-08-18 16:14:23 +01:00
IRHM bdf9b1d3d0 More provider icons 2023-08-18 02:39:30 +01:00
IRHM db6304e5f9 Make ProvidersList and ProviderIcon components
moved out and separated providers code
2023-08-18 01:15:48 +01:00
IRHM a61f0dbbf3 Filter watch providers to a specific country on server
before sending to client
2023-08-18 00:17:29 +01:00
IRHM 84cd421368 Format content pages 2023-08-17 02:28:58 +01:00
Mr 9c32a73311 Merge pull request #97 from DNYLA/dev
Streaming Providers shown for content
2023-08-17 01:20:00 +00:00
Dan cf90b8537b Merge branch 'sbondCo:dev' into dev 2023-08-17 02:15:57 +01:00
Mr 43da5706ed Merge pull request #95 from sbondCo/logging
Server Logging To File
2023-08-17 00:40:58 +00:00
IRHM 2e301c33a4 SeasonsList: Only show TBD for actual seasons
...not specials
2023-08-17 01:40:35 +01:00
IRHM 9dddac9820 Add missing env vars to readme and env example file 2023-08-17 01:34:12 +01:00
IRHM e9a17a0f4a Change lumberjack options
maxSize to 1mb, compress to false
2023-08-17 01:30:29 +01:00
IRHM 4482da9ec7 Make gin log to file 2023-08-17 01:09:17 +01:00
IRHM 0753346954 Update all logs to use slog 2023-08-17 00:59:26 +01:00
IRHM 4878e8d32b Start using slog for logging and lumberjack for writing to rotating file 2023-08-16 23:57:02 +01:00
Mr 86aef4fdd2 Merge pull request #92 from sbondCo/runtimes
Hide tv runtimes if not provided in episode_run_time array
2023-08-16 19:30:20 +00:00
IRHM 35dcaf11a1 Hide tv runtimes if not provided in episode_run_time array 2023-08-16 20:28:36 +01:00
DNYLA 2a69951d2e Added Import 2023-08-16 13:59:41 +01:00
DNYLA 67d17c5a82 Streaming Providers display 2023-08-16 13:52:27 +01:00
Mr bbd5533ac8 Merge pull request #90 from sbondCo/tbd
Show `TBD` if seasons not aired and no episodes message
2023-08-15 23:12:04 +00:00
IRHM e234524532 Show TBD if seasons not aired and no episodes message 2023-08-16 00:10:34 +01:00
Mr c4ec47ec97 Merge pull request #88 from sbondCo/deps
Update Client And Server Dependencies
2023-08-15 22:39:18 +00:00
IRHM 86b0f1a917 ugliggy lint 2023-08-15 23:35:41 +01:00
IRHM c929911187 Poster: enter on inner element go to content 2023-08-15 23:34:53 +01:00
IRHM 736a06d362 #87 Close VideoEmbedModal with escape key
Also fixed modal div linting issue
2023-08-15 23:23:18 +01:00
IRHM 755c4ca15f Fix linting issues - only extend svelte/recommended
ignore config.js files
2023-08-15 23:03:50 +01:00
IRHM a0fbb0ffef fix eslint and run format 2023-08-15 22:54:56 +01:00
IRHM e356ad604e Update server deps 2023-08-15 20:51:43 +01:00
IRHM 1c6df50622 Move to svelte 4
Update all related deps
2023-08-15 20:41:40 +01:00
IRHM 88796292be update most deps 2023-08-15 20:32:20 +01:00
Mr b97ea7076d Add contributing link and getting help section to README.md 2023-08-14 00:53:57 +00:00
Mr ee2e573083 Update CONTRIBUTING.md 2023-08-14 00:49:52 +00:00
IRHM c90f59bea3 v1.12.0 2023-08-14 01:16:47 +01:00
IRHM 373f758e54 1.12.0-dev1 2023-08-14 01:08:39 +01:00
IRHM 5e6cf832d8 SeasonsList: Round episode ratings 2023-08-14 01:06:59 +01:00
IRHM af0e1d7449 1.12.0-dev 2023-08-14 00:51:23 +01:00
Mr c53e06d79e Merge pull request #77 from sbondCo/episode-ratings-color
Fix episode ratings color
2023-08-13 23:49:41 +00:00
IRHM 060fbe9f3c Fix episode ratings color
Text is hard to read on light mode, just made it black. Other than gold, there isn't any nice yellowy color
2023-08-14 00:48:56 +01:00
Mr 4dc922ef46 Merge pull request #76 from sbondCo/trailers
Trailers
2023-08-13 23:41:56 +00:00
IRHM 850f348702 trailer on movies 2023-08-14 00:36:24 +01:00
IRHM b19f494166 trailer embed 2023-08-13 23:51:49 +01:00
IRHM 9293cbb79e view trailer button 2023-08-13 23:27:31 +01:00
IRHM d98834ed72 get trailers from tmdb 2023-08-13 22:26:10 +01:00
Mr a09e271258 Merge pull request #74 from sbondCo/episode-ratings
Episode ratings
2023-08-13 00:44:13 +00:00
IRHM 8b27d6dc71 SeasonsList: Fix episode rating overflow 2023-08-13 01:42:37 +01:00
IRHM 3cbc76513f SeasonsList: Add episode tmdb ratings 2023-08-13 01:34:34 +01:00
IRHM 4c4804c89b Activity: Fix month position bug
Seemed to occur when two months of activity, each month with one activity. The months would be put together.
2023-08-13 01:18:28 +01:00
Mr 09d121890b Merge pull request #73 from sbondCo/show-content-rating
Show content rating
2023-08-13 00:10:41 +00:00
IRHM fc170377f6 Content/Title: Show vote count in title 2023-08-13 01:05:00 +01:00
IRHM 773958d4e0 Content/Title: Update title on rating 2023-08-13 01:01:19 +01:00
IRHM 207ce5298d Show content ratings 2023-08-13 00:58:24 +01:00
IRHM b357e37b00 v1.11.0 2023-08-09 23:03:53 +01:00
IRHM e8ecc28631 1.11.0-dev 2023-08-09 22:46:50 +01:00
Mr cf92060c46 Merge pull request #69 from sbondCo/reviews
Add thoughts to watched & Show notifications on added/updated watched items
2023-08-09 21:33:17 +00:00
IRHM 205005e72d note to self 2023-08-09 22:33:02 +01:00
IRHM 86e7565782 Change updatewatched method so we can get original thoughts to save in activity 2023-08-09 22:29:17 +01:00
IRHM d4254f2e77 fix removing thoughts ugligly 2023-08-09 22:21:44 +01:00
IRHM 3a24b7cc8f Show success message for adding watched item 2023-08-08 23:07:15 +01:00
IRHM 5dad17a992 Fix status button on poster 2023-08-08 23:04:59 +01:00
IRHM effd7a6765 Fix notification styling 2023-08-08 23:04:49 +01:00
IRHM 543cbcfb47 Show success message on update watched item 2023-08-08 23:04:39 +01:00
IRHM 556d401acd textarea dark theme support 2023-08-08 21:33:39 +01:00
IRHM 824da069df Fix status buttons on light theme 2023-08-08 21:28:28 +01:00
IRHM 0c4649b162 Movie/Show thoughts
Add thoughts for YOUR thoughts on the content!! wooo yippeee
2023-08-08 21:25:29 +01:00
IRHM 8cc2f049c9 Content pages: only show cast is exists 2023-08-06 17:06:33 +01:00
IRHM fea8a4dce4 Fix activity month header 2023-08-06 17:04:26 +01:00
Mr 81755d60f9 Merge pull request #64 from sbondCo/profile-responsive
Make profile page responsive
2023-08-06 15:43:04 +00:00
IRHM 214ea7133e make profile page responsive 2023-08-06 16:41:08 +01:00
Mr df61b04878 Merge pull request #63 from sbondCo/themes
Themes
2023-08-06 15:14:13 +00:00
IRHM 1bd1f940d3 Persist theme selection 2023-08-06 15:55:59 +01:00
IRHM 3de1298a9c Fix spinner dark theme 2023-08-06 15:12:58 +01:00
IRHM bb7047d8df Fix poster dark theme 2023-08-06 15:09:17 +01:00
IRHM b97f939954 Different filter/mix-blend-mode for dark theme backdrop 2023-08-05 17:13:38 +01:00
IRHM bc9c568a32 Use theme colors 2023-08-05 17:00:18 +01:00
IRHM f524f79dde wip dark theme 2023-08-05 00:56:14 +01:00
IRHM 20fd6decf6 v1.10.0 2023-07-22 18:31:34 +01:00
IRHM beaf935c7f v1.10.0-dev2 2023-07-22 17:59:53 +01:00
Mr 1876f53ba4 Merge pull request #57 from sbondCo/fix-nav-dds
Nav DropDowns: Close other menu on open
2023-07-18 20:49:43 +00:00
IRHM 5df24a8327 Nav DropDowns: Close other menu on open 2023-07-18 20:41:44 +01:00
Mr 2ad979a7c5 Merge pull request #55 from sbondCo/profile
Profile
2023-07-18 19:36:38 +00:00
IRHM d8e18ad854 add margin to profile 2023-07-18 20:30:31 +01:00
IRHM 8450880cc9 profile: only count as watched if status is finished 2023-07-18 20:27:20 +01:00
IRHM 937e83b17b Create profile endpoint and add basic stats to page 2023-07-17 23:56:35 +01:00
IRHM 624d0eef23 Check episoderuntime len 2023-07-15 23:57:29 +01:00
IRHM 27315bb890 Add runtime, number of eps and seasons to content db 2023-07-15 23:45:22 +01:00
IRHM 15dfcfd477 start profile page 2023-07-14 23:27:29 +01:00
IRHM 2f2ed4ee4e Run debian 12 in runner dockerfile step 2023-07-13 23:54:24 +01:00
IRHM 971de1b458 Readme note on watcharr-ui package deprication 2023-07-13 23:02:46 +01:00
IRHM 3cedd75abd 1.10.0-dev 2023-07-13 22:43:20 +01:00
Mr 3486fb1521 Create CONTRIBUTING.md 2023-07-13 21:37:47 +00:00
Mr f26d7bb846 Merge pull request #53 from sbondCo/activity
Content Activity
2023-07-13 21:20:30 +00:00
IRHM 8e8fe57a6e Create .env.example
must have forgotten
2023-07-13 22:12:56 +01:00
IRHM 63d4d276fe Fix differences in returned and saved status/rating when readding content 2023-07-12 07:22:38 +01:00
IRHM 6fa49603a9 Only show activity is show is in watched list 2023-07-12 06:59:21 +01:00
IRHM b1e149a0ed Preload activities when adding show back
incase user has previous activities we should load
2023-07-12 06:56:35 +01:00
IRHM 7ac83d4744 removed from watched list msg 2023-07-12 06:43:43 +01:00
IRHM db26be7033 Update watched.go 2023-07-12 06:42:45 +01:00
IRHM 213264ad23 removed watched activity and soft deleting watched records 2023-07-12 06:40:33 +01:00
IRHM 751674e60f Change activity group name font 2023-07-12 05:33:20 +01:00
IRHM 28baca9e2a Group activities by month year 2023-07-12 05:30:24 +01:00
IRHM 70ecdf82d7 Make activity scrollable 2023-07-12 04:36:26 +01:00
IRHM 5b572edd34 Update activity when updating content 2023-07-12 04:34:36 +01:00
IRHM 8faa43b919 Add activity to watched item response when adding 2023-07-10 23:57:49 +01:00
IRHM 27f362f0a1 update error log in watched 2023-07-10 20:52:22 +01:00
IRHM 290654b17a show activity in movie page 2023-07-10 20:51:10 +01:00
IRHM 54178bd8e6 Save and show rating/status at add_watched time 2023-07-10 20:51:03 +01:00
IRHM 444e884d5f activity working
just needs to get live updates
2023-07-10 20:34:42 +01:00
IRHM 944492375b wip activity routes 2023-07-05 23:57:49 +01:00
IRHM 3fbef9171d style test activity component 2023-06-21 21:00:39 +01:00
Mr d2a08367e2 Merge pull request #52 from sbondCo/tv-eps
TV Seasons/Episodes
2023-06-21 20:33:14 +01:00
IRHM 7c3b8cd5ce Check for still_path before using in img tag 2023-06-18 21:31:06 +01:00
IRHM a1f9318ef3 Fix failed image styling 2023-06-18 21:26:20 +01:00
IRHM 7785a07bb0 Make seasons list responsive 2023-06-18 21:05:03 +01:00
IRHM c09f4a1be5 seasons scroll 2023-06-18 20:44:19 +01:00
IRHM 8abb29ac45 Show tv seasons/episodes 2023-06-18 12:38:38 +01:00
Mr 7313910483 Merge pull request #51 from sbondCo/filters
Watched Page Filters
2023-06-09 20:28:46 +01:00
IRHM 9625378eb5 Fix review section in content pages 2023-06-09 20:25:21 +01:00
IRHM ff0eb750bb Finalize filters menu 2023-06-06 12:21:30 +01:00
IRHM a8a3960705 store more info in content table 2023-06-05 22:02:12 +01:00
IRHM 0ecf8ef145 Create basic filter menu 2023-06-01 23:25:03 +01:00
Mr 707a748bb2 Update README.md 2023-06-01 19:28:26 +00:00
IRHM eec1d6a4c3 Update env var comment in README 2023-06-01 03:07:03 +01:00
IRHM 8e43ee21a7 v1.9.2 2023-06-01 02:58:05 +01:00
IRHM 80b808c39e Go back to adapter-node and run it in backend
proxies requests to it
2023-06-01 02:44:41 +01:00
IRHM eb27892e9a v1.9.1 2023-05-31 23:51:47 +01:00
Mr 889c7e32d2 Merge pull request #50 from sbondCo/docker-img-rework
Simplify Docker Images Into One
2023-05-31 22:46:48 +00:00
IRHM ca1a91e992 Update container-release.yml 2023-05-31 23:36:04 +01:00
IRHM 635eb7609c Update README setup steps 2023-05-31 23:34:31 +01:00
IRHM 3fefdfdfb8 Remove Caddyfile and update docker-compose example/test files 2023-05-31 23:33:15 +01:00
IRHM 43ffa6d694 Update test dev action 2023-05-31 23:12:39 +01:00
IRHM 23164858f8 Combine separated docker images into one
Server now also serves the frontend in production
2023-05-31 22:55:57 +01:00
IRHM 2134e78e47 1.10.0-dev 2023-05-31 00:53:06 +01:00
191 changed files with 42105 additions and 5102 deletions
+2
View File
@@ -7,6 +7,8 @@ node_modules
.env.*
!.env.example
/doc
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
+25 -18
View File
@@ -1,20 +1,27 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': () => require('typescript')
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020
},
env: {
browser: true,
es2017: true,
node: false
}
root: true,
parser: "@typescript-eslint/parser",
extends: ["plugin:svelte/recommended"],
plugins: ["@typescript-eslint"],
ignorePatterns: ["*.cjs", "*.config.js"],
overrides: [
{
files: ["*.svelte"],
parser: "svelte-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser"
}
}
],
parserOptions: {
project: "./tsconfig.json",
extraFileExtensions: [".svelte"],
sourceType: "module",
ecmaVersion: 2020
},
env: {
browser: true,
es2017: true,
node: false
}
};
+5
View File
@@ -0,0 +1,5 @@
<!-- Make sure your code is formatted by running `npm run format` or using prettier manually. -->
### Changes made
<!-- Describe changes made here. If changes are visual a screenshot could be useful! -->
+27
View File
@@ -0,0 +1,27 @@
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
# Web UI
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "ui"
# Server
- package-ecosystem: "gomod"
directory: "/server"
schedule:
interval: "weekly"
commit-message:
prefix: "server"
# Workflows
- package-ecosystem: "github-actions"
directory: "/" # / will look in /.github/workflows directory by default
schedule:
interval: "weekly"
commit-message:
prefix: "workflow"
+21 -18
View File
@@ -1,42 +1,45 @@
name: Development Watcharr Container Release
# Multi arch support doc/guide:
# https://docs.docker.com/build/ci/github-actions/multi-platform/
name: Development Watcharr Image Release
on:
workflow_dispatch:
env:
image: ghcr.io/sbondco/watcharr
jobs:
release-images:
release-dev-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: true
# Run for each dockerfile (one for server and one for ui images)
matrix:
include:
- dockerfile: ./Dockerfile.svr
image: ghcr.io/sbondco/watcharr
- dockerfile: ./Dockerfile.ui
image: ghcr.io/sbondco/watcharr-ui
steps:
- name: Checkout Repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: ${{ matrix.dockerfile }}
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ matrix.image }}:dev
labels: ${{ matrix.image }}:${{ github.sha }}
tags: ${{ env.image }}:dev
labels: ${{ env.image }}:${{ github.sha }}
+17 -18
View File
@@ -1,11 +1,13 @@
name: Release Watcharr Containers
name: Release Watcharr Image
on:
workflow_dispatch:
push:
tags:
- v*
env:
image: ghcr.io/sbondco/watcharr
jobs:
release-images:
runs-on: ubuntu-latest
@@ -14,22 +16,18 @@ jobs:
contents: read
packages: write
strategy:
fail-fast: true
# Run for each dockerfile (one for server and one for ui images)
matrix:
include:
- dockerfile: ./Dockerfile.svr
image: ghcr.io/sbondco/watcharr
- dockerfile: ./Dockerfile.ui
image: ghcr.io/sbondco/watcharr-ui
steps:
- name: Checkout Repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -37,15 +35,16 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: ${{ matrix.image }}
images: ${{ env.image }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: ${{ matrix.dockerfile }}
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+50
View File
@@ -0,0 +1,50 @@
name: Publish Docs
on:
push:
branches:
- dev
paths:
- ".github/workflows/publish-docs.yml"
- "doc/**"
permissions:
contents: write
jobs:
deploy:
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
defaults:
run:
working-directory: doc
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
cache: npm
- name: Install dependencies
run: npm ci
- name: Build website
run: npm run build
# Popular action to deploy to GitHub Pages:
# Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# Build output to publish to the `gh-pages` branch:
publish_dir: ./doc/build
cname: watcharr.app
# The following lines assign commit authorship to the official
# GH-Actions bot for deploys to `gh-pages` branch:
# https://github.com/actions/checkout/issues/13#issuecomment-724415212
# The GH actions bot is used by default if you didn't specify the two fields.
# You can swap them out with your own user credentials.
user_name: github-actions[bot]
user_email: 41898282+github-actions[bot]@users.noreply.github.com
+28
View File
@@ -0,0 +1,28 @@
name: Test doc deployment
on:
pull_request:
branches:
- dev
paths:
- ".github/workflows/test-docs.yml"
- "doc/**"
jobs:
test-deploy:
name: Test deployment
runs-on: ubuntu-latest
defaults:
run:
working-directory: doc
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
cache: npm
- name: Install dependencies
run: npm ci
- name: Test build website
run: npm run build
+28
View File
@@ -0,0 +1,28 @@
name: Test PR Changes
on:
pull_request:
branches:
- "**"
tags-ignore:
- v*
jobs:
linter_check:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Get dependencies
run: npm i
- name: Check for linting/formatting errors
run: npm run lint
+8
View File
@@ -37,3 +37,11 @@ node_modules
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Docker dev volume
container_data/
data/
# Build artifacts
server/ui
server/watcharr
+17 -22
View File
@@ -1,23 +1,18 @@
{
"tabWidth": 2,
"semi": true,
"singleQuote": false,
"printWidth": 100,
"useTabs": false,
"trailingComma": "none",
"arrowParens": "always",
"plugins": [
"prettier-plugin-svelte"
],
"pluginSearchDirs": [
"."
],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}
"tabWidth": 2,
"semi": true,
"singleQuote": false,
"printWidth": 100,
"useTabs": false,
"trailingComma": "none",
"arrowParens": "always",
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}
+15
View File
@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}"
}
]
}
+11
View File
@@ -0,0 +1,11 @@
# Contributing 🎉
First of all, thank you for looking into contributing! 🫡
Feel free to contribute in any way: 🐞 bug reports, 💡 ideas, 🥖 new features etc - everything is welcome!
For big PRs (lots of changes/big change in the way something works), if you'd like, it can be discussed first in an issue (dont want to waste anyones time!).
## Project Board
We have a [board to organize new features and bug fixes](https://github.com/orgs/sbondCo/projects/9/views/2) to be worked on. If you see something you'd like to work on, just ask in the issue and it can be assigned to you. Even if it isn't set for the next milestone already, it can be.
-11
View File
@@ -1,11 +0,0 @@
:80 {
# Reverse proxy for frontend
#reverse_proxy 172.17.0.1:3000
reverse_proxy watcharr:3000
# Route requests to /api/ to backend
handle_path /api/* {
#reverse_proxy 172.17.0.1:3080
reverse_proxy watcharr-server:3080
}
}
+35
View File
@@ -0,0 +1,35 @@
# Backend
FROM golang:1.21-alpine AS server
WORKDIR /server
COPY server/*.go server/go.* ./
COPY server/arr/*.go ./arr/
COPY server/game/*.go ./game/
# Required so we can build with cgo
RUN apk update && apk add --no-cache musl-dev gcc build-base
# CGO_CFLAGS: https://github.com/mattn/go-sqlite3/issues/1164#issuecomment-1635253695
RUN go mod download && GOOS=linux CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build -o ./watcharr
# Frontend
FROM node:20-alpine AS ui
WORKDIR /app
COPY package*.json vite.config.ts svelte.config.js tsconfig.json ./
COPY ./src ./src
COPY ./static ./static
RUN npm install && npm run build
# Production
FROM node:20-alpine AS runner
COPY --from=server /server/watcharr /
COPY --from=ui /app/build /ui
COPY --from=ui /app/package.json /app/package-lock.json /ui
RUN cd /ui && npm ci --omit=dev
EXPOSE 3080
CMD ["/watcharr"]
-18
View File
@@ -1,18 +0,0 @@
# Backend
FROM golang:1.20 AS server
WORKDIR /server
COPY server/*.go server/go.* ./
RUN go mod download && GOOS=linux go build -o ./watcharr
# Production
FROM debian:11.6 AS runner
RUN apt-get update && apt-get install ca-certificates -y
COPY --from=server /server/watcharr /
EXPOSE 3080
CMD ["/watcharr"]
-20
View File
@@ -1,20 +0,0 @@
# Frontend
FROM node:19 AS ui
WORKDIR /app
COPY package*.json vite.config.ts svelte.config.js tsconfig.json ./
COPY ./src ./src
COPY ./static ./static
RUN npm install && npm run build
# Production
FROM node:19 AS runner
WORKDIR /app
COPY --from=ui /app/package*.json /app/build ./
RUN npm ci --only=production
EXPOSE 3000
CMD ["node", "index.js"]
+42 -74
View File
@@ -1,92 +1,60 @@
<h1 align="center">Watcharr</h1>
<p align="center"><img src="./static/logo-col.png" alt="logo" /></p>
<p align="center"><img src="./static/logo-col.png" alt="logo" width="250" /></p>
<p align="center">
<a href="https://github.com/sbondCo/Watcharr/pkgs/container/watcharr"><img src="https://img.shields.io/github/v/release/sbondCo/Watcharr?label=version&style=for-the-badge" /></a>
<a href="https://beta.watcharr.app"><img src="https://img.shields.io/website?label=DEMO&style=for-the-badge&url=https%3A%2F%2Fbeta.watcharr.app" /></a>
<a href="https://watcharr.app"><img src="https://img.shields.io/website?label=DOCS&style=for-the-badge&url=https%3A%2F%2Fwatcharr.app" /></a>
<a href="https://github.com/sbondCo/Watcharr/issues"><img src="https://img.shields.io/github/issues-raw/sbondCo/Watcharr?label=ISSUES&style=for-the-badge" /></a>
<a href="/LICENSE"><img src="https://img.shields.io/github/license/sbondCo/Watcharr?style=for-the-badge" /></a>
<a href="https://matrix.to/#/#watcharr:matrix.org"><img src="https://img.shields.io/matrix/watcharr%3Amatrix.org?style=for-the-badge&logo=matrix" /></a>
</p>
I'm the place you store your watched content for later rememberance, sadly I am still in my infancy. I aspire to be easily self hosted.
I'm your new easily self-hosted content watched list. The place you store your watched (or watching, planned, etc) **movies** and **tv shows** (and **anime**), rate them and track their status.
With [some extra configuration](https://watcharr.app/docs/server_config/game-support-igdb) I can also track your **video games**.
I am built with Go and Svelte(Kit).
Feel free to abuse this demo instance (nicely), which runs on the latest `dev` build (there may be bugs, as new features are tested on here too before release): [https://watcharr.lab.sbond.co/](https://watcharr.lab.sbond.co/)
Feel free to abuse this demo instance (nicely), which runs on the latest `dev` build (there may be bugs, as new features are tested on here too): [https://beta.watcharr.app/](https://beta.watcharr.app/)
[Track Progress Until Next Version](https://github.com/orgs/sbondCo/projects/9/views/3)
### Contents
- [Screenshots](#screenshots)
- [Setup](#set-up)
- [Contributing](CONTRIBUTING.md)
- [Getting Help](#getting-help)
# Screenshots
<h3 align="center">Watched List</h3>
<p align="center">
<img src="./screenshot/homepage.png" alt="Watched List" />
| Watched Show Hover | Watched Show Status Change | Show Details |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| <img src="./screenshot/homepage-poster-hover.png" alt="Watched List" /> | <img src="./screenshot/homepage-poster-change-status.png" alt="Changing Show Status" /> | <img src="./screenshot/show-details-page.png" alt="Content Details Page" /> |
| Homepage | Watched Show Hover |
| ---------------------------------------------------------- | ----------------------------------------------------------------------- |
| <img src="./screenshot/homepage.png" alt="Watched List" /> | <img src="./screenshot/homepage-poster-hover.png" alt="Watched List" /> |
| Watched Show Status Change | Movie Details |
| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| <img src="./screenshot/homepage-poster-change-status.png" alt="Changing Show Status" /> | <img src="./screenshot/content-details-page.png" alt="Content Details Page" /> |
| User Profile | Discover |
| ------------------------------------------------------------------- | ---------------------------------------------------------------- |
| <img src="./screenshot/user-profile.png" alt="User Profile Page" /> | <img src="./screenshot/discover-page.png" alt="Discover Page" /> |
| Dark Homepage | Dark Content Details |
| ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| <img src="./screenshot/homepage-dark.png" alt="Dark Theme Homepage" /> | <img src="./screenshot/content-details-page-dark.png" alt="Dark Theme Content Page" /> |
</p>
# Set Up
Currently we have docker images for each version that are fairly simple to setup, but it is a lot more complex that I would like it to be. Hopefully this improves in the future.
[Checkout our documentation](https://watcharr.app/docs/category/installation) for an up to date guide on setup! If you hate manuals, but love docker, this [docker-compose.yml](./docker-compose.yml) file is your friend.
For now though, we have two container images ([UI](https://github.com/sbondCo/Watcharr/pkgs/container/watcharr-ui) & [Server](https://github.com/sbondCo/Watcharr/pkgs/container/watcharr-ui)) that work together through the use of a proxy.
# Getting Help
Here's a simple setup to get you started based from visible files in the repo, which you can also check out. We use Caddy here, but use whatever you are comfortable with!
If something isn't working for you or you are stuck, [creating an issue](https://github.com/sbondCo/Watcharr/issues/new) is the best way to get help! Every type of issue is accepted, so don't be afraid to ask anything!
**docker-compose.yml**
```
version: "3"
services:
# Watcharr API
watcharr:
image: ghcr.io/sbondco/watcharr:latest
container_name: watcharr
ports:
- 3080:3080
volumes:
# .env file to configure watcharr
- ./.env:/.env
# Contains all of watcharr data (database & cache)
- ./data:/data
# Watcharr Frontend
watcharr-ui:
image: ghcr.io/sbondco/watcharr-ui:latest
container_name: watcharr-ui
ports:
- 3000:3000
depends_on:
- watcharr
# Caddy - Used as reverse proxy to services
# and to re-route requests
caddy:
image: caddy:2.6
container_name: watcharr-caddy
ports:
- "8080:80"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
```
**Caddyfile**
```
:80 {
# Reverse proxy for frontend
reverse_proxy watcharr:3000
# Route requests to /api/ to backend
handle_path /api/* {
reverse_proxy watcharr-server:3080
}
}
```
**.env**
```
# Used to sign JWT tokens. Make sure to make
# it strong, just like a very long, complicated password.
JWT_SECRET=MAKE_ME_RANDOM_AND_LONG
# Optional: Point to your Jellyfin install
# to enable it as an auth provider.
JELLYFIN_HOST=https://my.jellyfin.example
```
You can also [join our space on Matrix](https://matrix.to/#/#watcharr:matrix.org) for support.
+20
View File
@@ -0,0 +1,20 @@
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+41
View File
@@ -0,0 +1,41 @@
# Website
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
### Installation
```
$ yarn
```
### Local Development
```
$ yarn start
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
### Build
```
$ yarn build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
### Deployment
Using SSH:
```
$ USE_SSH=true yarn deploy
```
Not using SSH:
```
$ GIT_USER=<Your GitHub username> yarn deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
presets: [require.resolve("@docusaurus/core/lib/babel/preset")]
};
+8
View File
@@ -0,0 +1,8 @@
{
"label": "Installation",
"position": 2,
"link": {
"type": "generated-index",
"description": "Choose how you would like to install Watcharr."
}
}
+69
View File
@@ -0,0 +1,69 @@
---
sidebar_position: 1
---
# Docker Compose
## Installing
Installing Watcharr with a docker compose file is easy. You can copy the example below to get started:
```yaml title="docker-compose.yml"
version: "3"
services:
watcharr:
# The :latest tag is used for simplicity, it is recommended
# to use an actual version, then when updating check the releases for changelogs.
image: ghcr.io/sbondco/watcharr:latest
container_name: watcharr
ports:
- 3080:3080
volumes:
# Contains all of watcharr data (database & cache)
- ./data:/data
restart: unless-stopped
```
:::danger first account
When **first** running Watcharr, make sure only you have access. The first user created will become admin.
:::
You can now start `Watcharr` like so:
```bash
docker compose up -d
```
If you didn't change the ports in the example, the server will be available at [http://localhost:3080/](http://localhost:3080/).
## Updating
:::danger Take care
We try taking care as to not release breaking changes, however it is still recommended that
you lookover changelogs before updating!
Breaking changes are marked at the top of releases: https://github.com/sbondCo/Watcharr/releases
:::
Updating your server can be done in two steps:
1. Update the `image` version in your `docker-compose.yml` file.
Skip this step if you are using the `latest` tag.
```yaml
# eg. update v1.19.0 to v1.20.0 (or whatever version you are updating to)
image: ghcr.io/sbondco/watcharr:v1.19.0
```
2. Pull the new changes and re-create your container:
```bash
docker compose pull && docker compose down && docker compose up -d
```
And that is it!
+74
View File
@@ -0,0 +1,74 @@
---
sidebar_position: 10
---
# From Source
## Installing
1. Get the code
```bash
git clone https://github.com/sbondCo/Watcharr.git && \
cd Watcharr
```
If you want a specific version, checkout a version tag (replace `<tag>` with the tag):
```bash
git checkout <tag>
```
2. Build the frontend
The server will use the built files, so we move them to the `ui` folder, besides where it's binary will be built.
```bash
npm i && \
npm run build && \
mv ./build ./server/ui
```
3. Build the server
```bash
cd server && \
go mod download && \
GOOS=linux go build -o ./watcharr
```
4. Run the server
:::danger first account
When **first** running Watcharr, make sure only you have access. The first user created will become admin.
:::
```bash
./watcharr
```
5. Visit [http://localhost:3080/](http://localhost:3080/) and setup Watcharr.
## Updating
:::danger Take care
We try taking care as to not release breaking changes, however it is still recommended that
you lookover changelogs before updating!
Breaking changes are marked at the top of releases: https://github.com/sbondCo/Watcharr/releases
:::
Updating is the same as installing except:
1. Cleanup old build. Delete the `ui` folder and the `watcharr` binary.
2. Replace step one with getting the new code:
```bash
git pull
```
If you checked out a tag, update to the new tag as well.
+10
View File
@@ -0,0 +1,10 @@
---
sidebar_position: 1
---
# Introduction
Watcharr is for the peeps who like to keep a list of all the content they have ever watched.
If you would like more insight, the [adding to your watchlist guide](/docs/usage/adding-to-watchlist)
is a great example of how Watcharr is used.
+8
View File
@@ -0,0 +1,8 @@
{
"label": "Server Configuration",
"position": 2,
"link": {
"type": "generated-index",
"description": "Learn how to configure your server."
}
}
@@ -0,0 +1,47 @@
---
sidebar_position: 1
---
# Games Support (via IGDB/Twitch)
## Overview
Game support in Watcharr, currently, must be enabled by you.
This involves setting up a Twitch developer account (phone number needed for 2FA which is required) and entering application credentials into Watcharr, that can then be used for looking up game info via IGDB.
P.S. These steps are stolen from [igdb api docs](https://api-docs.igdb.com/#account-creation), I've tried to make it easier to follow with screenshots.
## Steps
### Setup On Twitch
1. Sign up for a [Twitch](https://dev.twitch.tv/login) account.
Click login with twitch, then create an account in the Sign Up tab.
2. Enable two factor authentication in your [account Security settings](https://www.twitch.tv/settings/security) (currently requires a phone number).
3. [Register a new Application in the Twitch Developer Console](https://dev.twitch.tv/console/apps/create).
These settings will do (name can be anything, we don't use OAuth, but a redirect url is required; localhost will do):
![Registering twitch application](./img/twitch-register-app.png)
4. [Click manage](https://dev.twitch.tv/console/apps) on your newly created application.
5. Click 'New Secret' to generate a new Client Secret.
6. Take a note of your Client ID and new Client Secret (by leaving this tab open, until we are ready to copy the values into Watcharr).
### Setup On Watcharr
1. Login to Watcharr with an Admin account and navigate to server settings.
![Navigating to server settings in Watcharr](./img/watcharr-settings-dropdown.png)
2. Scroll down and click on the Twitch service.
![Twitch service config button](./img/watcharr-twitch-service.png)
3. Enter your Client ID and Client Secret (from Twitch Application page, step 6 on twitch setup steps)
![Twitch service config](./img/watcharr-twitch-config.png)
4. Click save, and start searching for your favourite games!
![Watcharr game details page](./img/watcharr-game-details.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

+8
View File
@@ -0,0 +1,8 @@
{
"label": "Usage",
"position": 3,
"link": {
"type": "generated-index",
"description": "Hopefully Watcharr is easy enough to get started with and use, but incase you are stuck, one of these usage guides may help."
}
}
+39
View File
@@ -0,0 +1,39 @@
---
sidebar_position: 1
---
# Adding To Your Watchlist
Adding movies or shows to your watchlist is the core feature of Watcharr.
The homepage is where your list will appear once you have added some.
To get started, you can follow along with these steps to add your first movie/show:
1. Search for the content:
![Searching For Content](./img/search-for-content.png)
2. Hover or click on the content you want to add:
![Hovering over poster](./img/search-for-content-hover-poster.png)
3. Clicking the + (Plus) icon will let you give the content a status.
Currently supported statuses include: `Finished`, `Watching`, `Held`, `Planned`, `Dropped`.
![Giving content a status](./img/search-for-content-hover-poster-status.png)
4. Rate the content.
![Giving content a rating](./img/search-for-content-hover-poster-rate.png)
If you prefer, you can also do this by clicking/tapping on the active poster again.
This will take you to the contents full page. Here you can see all the options, including
adding `Thoughts` for the content.
![Content full page](./img/content-full-page.png)
`Thoughts` can be anything you would like to write down or remember about the specific movie/show.
For example, sometimes I like to write something specific that I _really_ liked about the content,
or for some shows, I write down my favourite seasons and how many times I have watched them.
Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

+110
View File
@@ -0,0 +1,110 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
const { themes } = require("prism-react-renderer");
const lightTheme = themes.github;
const darkTheme = themes.dracula;
/** @type {import('@docusaurus/types').Config} */
const config = {
title: "Watcharr Docs",
tagline:
"Open source, self-hostable watched list for all your content with user authentication, modern and clean UI and a very simple setup. ",
favicon: "img/favicon.png",
// Set the production url of your site here
url: "https://watcharr.app",
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: "/",
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: "sbondCo", // Usually your GitHub org/user name.
projectName: "Watcharr", // Usually your repo name.
onBrokenLinks: "throw",
onBrokenMarkdownLinks: "warn",
trailingSlash: false,
// Even if you don't use internalization, you can use this field to set useful
// metadata like html lang. For example, if your site is Chinese, you may want
// to replace "en" with "zh-Hans".
i18n: {
defaultLocale: "en",
locales: ["en"]
},
presets: [
[
"classic",
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
sidebarPath: require.resolve("./sidebars.js"),
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl: "https://github.com/sbondCo/Watcharr/tree/dev/doc"
},
blog: {
showReadingTime: true,
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl: "https://github.com/sbondCo/Watcharr/tree/dev/doc"
},
theme: {
customCss: [
"./node_modules/viewerjs/dist/viewer.css",
require.resolve("./src/css/custom.css")
]
}
})
]
],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
image: "img/social-card.png",
navbar: {
title: "Watcharr",
logo: {
alt: "Watcharr Logo",
src: "img/favicon.png"
},
items: [
{
type: "docSidebar",
sidebarId: "tutorialSidebar",
position: "left",
label: "Docs"
},
{
type: "docsVersionDropdown"
},
{
href: "https://beta.watcharr.app",
label: "Demo",
position: "right"
},
{
href: "https://github.com/sbondCo/Watcharr",
label: "GitHub",
position: "right"
}
]
},
prism: {
theme: lightTheme,
darkTheme: darkTheme
},
colorMode: {
defaultMode: "light",
// Dark theme currently disabled.. no time to fix the icons etc.
disableSwitch: true
}
})
};
module.exports = config;
+14664
View File
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
{
"name": "watcharr-doc",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "3.1.1",
"@docusaurus/preset-classic": "3.1.1",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.1.0",
"prism-react-renderer": "^2.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"viewerjs": "^1.11.6"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.1.1"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"engines": {
"node": ">=18.0"
}
}
+33
View File
@@ -0,0 +1,33 @@
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
*/
// @ts-check
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
// By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [{ type: "autogenerated", dirName: "." }]
// But you can create a sidebar manually
/*
tutorialSidebar: [
'intro',
'hello',
{
type: 'category',
label: 'Tutorial',
items: ['tutorial-basics/create-a-document'],
},
],
*/
};
module.exports = sidebars;
@@ -0,0 +1,61 @@
import React from "react";
import clsx from "clsx";
import styles from "./styles.module.css";
const FeatureList = [
{
title: "Free and Open",
Svg: require("@site/static/img/icon/code-slash.svg").default,
description: (
<>
Watcharr is free and open source software distributed under the MIT license. Feel free to
browse, modify or contribute!
</>
)
},
{
title: "Simple",
Svg: require("@site/static/img/icon/happy.svg").default,
description: (
<>
Watcharr is incredibly easy to dive into. Install without hassle and get started with the
seamless and intuitive user experience.
</>
)
},
{
title: "Built With Go and Svelte",
Svg: require("@site/static/img/icon/go.svg").default,
description: (
<>Don't know what else to add here, so now you know Watcharr is built with Go and Svelte.</>
)
}
];
function Feature({ Svg, title, description }) {
return (
<div className={clsx("col col--4")}>
<div className="text--center">
<Svg className={styles.featureSvg} role="img" />
</div>
<div className="text--center padding-horiz--md">
<h3>{title}</h3>
<p>{description}</p>
</div>
</div>
);
}
export default function HomepageFeatures() {
return (
<section className={styles.features}>
<div className="container">
<div className="row">
{FeatureList.map((props, idx) => (
<Feature key={idx} {...props} />
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,11 @@
.features {
display: flex;
align-items: center;
padding: 2rem 0;
width: 100%;
}
.featureSvg {
height: 200px;
width: 200px;
}
+71
View File
@@ -0,0 +1,71 @@
import React, { useEffect } from "react";
import styles from "./styles.module.css";
import clsx from "clsx";
import Viewer from "viewerjs";
function TwoColFeat({ title, description, imgSrc }) {
return (
<div className={clsx(styles.twoCol)}>
<div>
<h2>{title}</h2>
<p>{description}</p>
</div>
<div>
<img src={imgSrc} alt="" loading="lazy" />
</div>
</div>
);
}
export function HomepageTwoColFeats() {
useEffect(() => {
const ctr = document.getElementById("homepage-two-col-feats");
console.debug("Starting image viewer", ctr);
new Viewer(ctr, {
navbar: 0,
toolbar: {
zoomIn: 2,
zoomOut: 2,
next: 2,
prev: 2
},
title: 0,
className: "twoColViewer"
});
}, []);
return (
<section id="homepage-two-col-feats" className={styles.twoColFeats}>
<TwoColFeat
title="Neat UI"
description="We just look lightweight."
imgSrc="https://github.com/sbondCo/Watcharr/raw/dev/screenshot/homepage.png"
/>
<TwoColFeat
title="Easy To Use"
description="Not much to it, other than ease of use."
imgSrc="https://github.com/sbondCo/Watcharr/raw/dev/screenshot/homepage-poster-change-status.png"
/>
<TwoColFeat
title="In-Depth Details"
description="About your favourite content."
imgSrc="https://github.com/sbondCo/Watcharr/raw/dev/screenshot/content-details-page.png"
/>
<TwoColFeat
title="Personal Stats and Configuration"
description="Make it how you like."
imgSrc="https://github.com/sbondCo/Watcharr/raw/dev/screenshot/user-profile.png"
/>
<TwoColFeat
title="Discovery"
description="Find the latest trending content."
imgSrc="https://github.com/sbondCo/Watcharr/raw/dev/screenshot/discover-page.png"
/>
<TwoColFeat
title="Dark Theme"
description="For you who are still unsatisfied. ;("
imgSrc="https://github.com/sbondCo/Watcharr/raw/dev/screenshot/content-details-page-dark.png"
/>
</section>
);
}
@@ -0,0 +1,53 @@
.twoColFeats {
display: flex;
align-items: center;
margin-left: auto;
margin-right: auto;
flex-flow: column;
width: 100%;
max-width: 1300px;
}
.twoCol {
display: flex;
flex-flow: row;
width: 100%;
padding: 2.5rem 5rem;
gap: 50px;
background-color: rgb(236, 236, 236);
}
.twoCol:nth-child(even) {
flex-flow: row-reverse;
background-color: transparent;
}
.twoCol > div {
width: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-flow: column;
}
.twoCol > div > img {
width: 100%;
border-radius: 12px;
box-shadow: 0px 0px 5px rgb(121, 121, 121);
cursor: pointer;
}
@media screen and (max-width: 850px) {
.twoCol,
.twoCol:nth-child(even) {
flex-flow: column;
}
.twoCol > div {
width: 100%;
}
.twoCol > div > img {
max-width: 500px;
}
}
+44
View File
@@ -0,0 +1,44 @@
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #2e8555;
--ifm-color-primary-dark: #29784c;
--ifm-color-primary-darker: #277148;
--ifm-color-primary-darkest: #205d3b;
--ifm-color-primary-light: #33925d;
--ifm-color-primary-lighter: #359962;
--ifm-color-primary-lightest: #3cad6e;
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme="dark"] {
--ifm-color-primary: #25c2a0;
--ifm-color-primary-dark: #21af90;
--ifm-color-primary-darker: #1fa588;
--ifm-color-primary-darkest: #1a8870;
--ifm-color-primary-light: #29d5b0;
--ifm-color-primary-lighter: #32d8b4;
--ifm-color-primary-lightest: #4fddbf;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}
.twoColViewer .viewer-close {
right: 10px;
top: 10px;
width: 50px;
height: 50px;
}
/* Stupid poopin fix for viewerjs because whatever is bundling the css
isn't keeping the order of this correctly. All this time wasted and it
was just opaque ;( BUT ONLY IN THE BUILD AAAAAAAAAAAAAAAAAAAAAA */
.viewer-in {
opacity: 1 !important;
}
+53
View File
@@ -0,0 +1,53 @@
import React from "react";
import Link from "@docusaurus/Link";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import Layout from "@theme/Layout";
import HomepageFeatures from "@site/src/components/HomepageFeatures";
import LogoCol from "@site/static/img/logo-col.png";
import styles from "./index.module.css";
import clsx from "clsx";
import { HomepageTwoColFeats } from "../components/TwoColFeat";
function HomepageHeader() {
const { siteConfig } = useDocusaurusContext();
return (
<header className="hero shadow--lw">
<div className={styles.heroContainer}>
<div className={styles.heroWrapper}>
<img src={LogoCol} />
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link className="button button--secondary button--lg" to="/docs/introduction">
Get Started
</Link>
<Link
className="button button--secondary button--outline button--lg"
to="https://beta.watcharr.app"
target="_blank"
>
Demo
</Link>
</div>
</div>
</div>
</header>
);
}
export default function Home() {
const { siteConfig } = useDocusaurusContext();
return (
<Layout
title={`${siteConfig.title}`}
description="Description will go into a meta tag in <head />"
>
<HomepageHeader />
<main>
<HomepageFeatures />
<HomepageTwoColFeats />
</main>
</Layout>
);
}
+34
View File
@@ -0,0 +1,34 @@
/**
* CSS files with the .module.css suffix will be treated as CSS modules
* and scoped locally.
*/
.heroContainer {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.heroWrapper {
display: flex;
align-items: center;
justify-content: center;
flex-flow: column;
max-width: 600px;
padding-left: 2rem;
padding-right: 2rem;
text-align: center;
}
.heroWrapper img {
width: 30vh;
max-width: 250px;
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
+7
View File
@@ -0,0 +1,7 @@
---
title: Markdown page example
---
# Markdown page example
You don't need React to write simple standalone pages.
View File
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path d="M160 389a20.91 20.91 0 01-13.82-5.2l-128-112a21 21 0 010-31.6l128-112a21 21 0 0127.66 31.61L63.89 256l109.94 96.19A21 21 0 01160 389zM352 389a21 21 0 01-13.84-36.81L448.11 256l-109.94-96.19a21 21 0 0127.66-31.61l128 112a21 21 0 010 31.6l-128 112A20.89 20.89 0 01352 389zM208 437a21 21 0 01-20.12-27l96-320a21 21 0 1140.23 12l-96 320A21 21 0 01208 437z"/></svg>

After

Width:  |  Height:  |  Size: 447 B

+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Go</title><path d="M1.811 10.231c-.047 0-.058-.023-.035-.059l.246-.315c.023-.035.081-.058.128-.058h4.172c.046 0 .058.035.035.07l-.199.303c-.023.036-.082.07-.117.07zM.047 11.306c-.047 0-.059-.023-.035-.058l.245-.316c.023-.035.082-.058.129-.058h5.328c.047 0 .07.035.058.07l-.093.28c-.012.047-.058.07-.105.07zm2.828 1.075c-.047 0-.059-.035-.035-.07l.163-.292c.023-.035.07-.07.117-.07h2.337c.047 0 .07.035.07.082l-.023.28c0 .047-.047.082-.082.082zm12.129-2.36c-.736.187-1.239.327-1.963.514-.176.046-.187.058-.34-.117-.174-.199-.303-.327-.548-.444-.737-.362-1.45-.257-2.115.175-.795.514-1.204 1.274-1.192 2.22.011.935.654 1.706 1.577 1.835.795.105 1.46-.175 1.987-.77.105-.13.198-.27.315-.434H10.47c-.245 0-.304-.152-.222-.35.152-.362.432-.97.596-1.274a.315.315 0 01.292-.187h4.253c-.023.316-.023.631-.07.947a4.983 4.983 0 01-.958 2.29c-.841 1.11-1.94 1.8-3.33 1.986-1.145.152-2.209-.07-3.143-.77-.865-.655-1.356-1.52-1.484-2.595-.152-1.274.222-2.419.993-3.424.83-1.086 1.928-1.776 3.272-2.02 1.098-.2 2.15-.07 3.096.571.62.41 1.063.97 1.356 1.648.07.105.023.164-.117.2m3.868 6.461c-1.064-.024-2.034-.328-2.852-1.029a3.665 3.665 0 01-1.262-2.255c-.21-1.32.152-2.489.947-3.529.853-1.122 1.881-1.706 3.272-1.95 1.192-.21 2.314-.095 3.33.595.923.63 1.496 1.484 1.648 2.605.198 1.578-.257 2.863-1.344 3.962-.771.783-1.718 1.273-2.805 1.495-.315.06-.63.07-.934.106zm2.78-4.72c-.011-.153-.011-.27-.034-.387-.21-1.157-1.274-1.81-2.384-1.554-1.087.245-1.788.935-2.045 2.033-.21.912.234 1.835 1.075 2.21.643.28 1.285.244 1.905-.07.923-.48 1.425-1.228 1.484-2.233z"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path d="M414.39 97.61A224 224 0 1097.61 414.39 224 224 0 10414.39 97.61zM184 208a24 24 0 11-24 24 23.94 23.94 0 0124-24zm167.67 106.17c-12 40.3-50.2 69.83-95.62 69.83s-83.62-29.53-95.72-69.83a8 8 0 017.83-10.17h175.69a8 8 0 017.82 10.17zM328 256a24 24 0 1124-24 23.94 23.94 0 01-24 24z"/></svg>

After

Width:  |  Height:  |  Size: 373 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

+3 -29
View File
@@ -1,39 +1,13 @@
version: "3"
networks:
watcharr:
# To test prod
services:
# Port 3000
watcharr:
build:
context: .
dockerfile: Dockerfile.ui
dockerfile: Dockerfile
container_name: watcharr
networks:
- watcharr
depends_on:
- watcharr-server
# Port 3080
watcharr-server:
build:
context: .
dockerfile: Dockerfile.svr
container_name: watcharr-server
networks:
- watcharr
volumes:
- ./server/.env:/.env
- ./container_data:/data
caddy:
image: caddy:2.6
container_name: watcharr-caddy
ports:
- "8080:80"
networks:
- watcharr
- 3080:3080
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./container_data:/data
+5 -20
View File
@@ -1,29 +1,14 @@
version: "3"
services:
# Watcharr API
watcharr:
# The :latest tag is used for simplicity, it is recommended
# to use an actual version, then when updating check the releases for changelogs.
image: ghcr.io/sbondco/watcharr:latest
container_name: watcharr
ports:
- 3080:3080
volumes:
- ./server/.env:/.env
- ./container_data:/data
# Watcharr Frontend
watcharr-ui:
image: ghcr.io/sbondco/watcharr-ui:latest
container_name: watcharr-ui
ports:
- 3000:3000
depends_on:
- watcharr
caddy:
image: caddy:2.6
container_name: watcharr-caddy
ports:
- "8080:80"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
# Contains all of watcharr data (database & cache)
- ./data:/data
restart: unless-stopped
+8043 -3214
View File
File diff suppressed because it is too large Load Diff
+39 -32
View File
@@ -1,34 +1,41 @@
{
"name": "watcharr",
"version": "1.9.0",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@sveltejs/adapter-node": "^1.2.3",
"@sveltejs/kit": "^1.15.2",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"eslint": "^8.38.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-svelte3": "^4.0.0",
"prettier": "^2.8.7",
"prettier-plugin-svelte": "^2.10.0",
"svelte": "^3.58.0",
"svelte-check": "^3.2.0",
"tslib": "^2.5.0",
"typescript": "^5.0.4",
"vite": "^4.2.1"
},
"type": "module",
"dependencies": {
"axios": "^1.3.4"
}
"name": "watcharr",
"version": "1.36.0",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"server": "cd ./server && MODE=DEV go run ."
},
"devDependencies": {
"@sveltejs/adapter-node": "^4.0.1",
"@sveltejs/kit": "^2.5.3",
"@types/papaparse": "^5.3.14",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vite-pwa/sveltekit": "^0.3.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.1",
"svelte": "^4.2.10",
"svelte-check": "^3.6.4",
"svelte-eslint-parser": "^0.33.1",
"svelte-preprocess": "^5.1.3",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"vite": "^5.1.6"
},
"type": "module",
"dependencies": {
"axios": "^1.6.7",
"blurhash": "^2.0.5",
"papaparse": "^5.4.1"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 KiB

After

Width:  |  Height:  |  Size: 743 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 KiB

After

Width:  |  Height:  |  Size: 752 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 425 KiB

After

Width:  |  Height:  |  Size: 816 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+126
View File
@@ -0,0 +1,126 @@
package main
import (
"errors"
"log/slog"
"time"
"gorm.io/gorm"
)
type ActivityType string
var (
ADDED_WATCHED ActivityType = "ADDED_WATCHED"
REMOVED_WATCHED ActivityType = "REMOVED_WATCHED"
RATING_CHANGED ActivityType = "RATING_CHANGED"
STATUS_CHANGED ActivityType = "STATUS_CHANGED"
THOUGHTS_CHANGED ActivityType = "THOUGHTS_CHANGED"
THOUGHTS_REMOVED ActivityType = "THOUGHTS_REMOVED"
IMPORTED_WATCHED ActivityType = "IMPORTED_WATCHED"
IMPORTED_WATCHED_JF ActivityType = "IMPORTED_WATCHED_JF"
IMPORTED_WATCHED_PLEX ActivityType = "IMPORTED_WATCHED_PLEX"
IMPORTED_RATING ActivityType = "IMPORTED_RATING" // Imported rating, but with no rating acts as original import of content to old platform (where they are importing from) activity
IMPORTED_ADDED_WATCHED ActivityType = "IMPORTED_ADDED_WATCHED" // Imported watched date, so we can save the original watch dates of content from users old platform (where they are importing from).
IMPORTED_ADDED_WATCHED_JF ActivityType = "IMPORTED_ADDED_WATCHED_JF"
IMPORTED_ADDED_WATCHED_PLEX ActivityType = "IMPORTED_ADDED_WATCHED_PLEX"
SEASON_ADDED ActivityType = "SEASON_ADDED"
SEASON_ADDED_JF ActivityType = "SEASON_ADDED_JF"
SEASON_ADDED_PLEX ActivityType = "SEASON_ADDED_PLEX"
SEASON_REMOVED ActivityType = "SEASON_REMOVED"
SEASON_RATING_CHANGED ActivityType = "SEASON_RATING_CHANGED"
SEASON_STATUS_CHANGED ActivityType = "SEASON_STATUS_CHANGED"
EPISODE_ADDED ActivityType = "EPISODE_ADDED"
EPISODE_ADDED_JF ActivityType = "EPISODE_ADDED_JF"
EPISODE_ADDED_PLEX ActivityType = "EPISODE_ADDED_PLEX"
EPISODE_REMOVED ActivityType = "EPISODE_REMOVED"
EPISODE_RATING_CHANGED ActivityType = "EPISODE_RATING_CHANGED"
EPISODE_STATUS_CHANGED ActivityType = "EPISODE_STATUS_CHANGED"
)
type Activity struct {
GormModel
// ID of user this activity is linked to, so it can be easily
// secured (users can only view their own activities).
UserID uint `json:"-" gorm:"not null"`
// ID of watched list item this activity is linked to.
WatchedID uint `json:"watchedId" gorm:"not null"`
// Type of activity.
Type ActivityType `json:"type" gorm:"not null"`
// Holds custom data (ex, if rating changed, this can
// hold new rating - if status changed, this will hold that).
Data string `json:"data" gorm:"not null"`
// Custom date for the activity, that the user can define.
CustomDate *time.Time `json:"customDate,omitempty"`
}
type ActivityAddRequest struct {
WatchedID uint `json:"watchedId" binding:"required"`
Type ActivityType `json:"type" binding:"required"`
Data string `json:"data" binding:"required"`
CustomDate *time.Time `json:"customDate,omitempty"`
}
type ActivityUpdateRequest struct {
CustomDate time.Time `json:"customDate" binding:"required"`
}
func getActivity(db *gorm.DB, userId uint, watchedId uint) ([]Activity, error) {
activity := new([]Activity)
res := db.Model(&Activity{}).Where("user_id = ? AND watched_id = ?", userId, watchedId).Find(&activity)
if res.Error != nil {
slog.Error("Failed getting activity from database", "error", res.Error.Error())
return []Activity{}, errors.New("failed getting activity")
}
return *activity, nil
}
func addActivity(db *gorm.DB, userId uint, ar ActivityAddRequest) (Activity, error) {
if ar.WatchedID == 0 {
return Activity{}, errors.New("watchedId must be set to add an activity")
}
activity := Activity{UserID: userId, WatchedID: ar.WatchedID, Type: ar.Type, Data: ar.Data, CustomDate: ar.CustomDate}
res := db.Create(&activity)
if res.Error != nil {
slog.Error("Error adding activity to database", "error", res.Error.Error())
return Activity{}, errors.New("failed adding new activity to database")
}
slog.Debug("Adding activity", "added_activity", activity)
return activity, nil
}
func updateActivity(db *gorm.DB, userId uint, id uint, activityUpdateRequest ActivityUpdateRequest) error {
if id == 0 {
return errors.New("id must be set to update an activity")
}
if activityUpdateRequest.CustomDate.IsZero() {
return errors.New("customDate must be set to update an activity")
}
res := db.Model(&Activity{}).Where("user_id = ? AND id = ?", userId, id).Update("custom_date", activityUpdateRequest.CustomDate)
if res.Error != nil {
slog.Error("Error updating activity in database", "error", res.Error.Error())
return errors.New("failed updating activity in database")
}
if res.RowsAffected < 1 {
slog.Error("No activities were updated. This may be because the activity doesn't exist or is not owned by the calling user.")
return errors.New("failed updating activity in database")
}
slog.Debug("Updating activity", "updated_activity", id)
return nil
}
func deleteActivity(db *gorm.DB, userId uint, id uint) error {
if id == 0 {
return errors.New("an id must be provided to delete an activity")
}
res := db.Where("user_id = ?", userId).Delete(&Activity{}, id)
if res.Error != nil {
slog.Error("Error deleting activity in database", "error", res.Error.Error())
return errors.New("failed deleting activity in database")
}
if res.RowsAffected < 1 {
slog.Error("No activities were deleted. This may be because the activity doesn't exist or is not owned by the calling user.")
return errors.New("failed deleting activity from database")
}
return nil
}
+204
View File
@@ -0,0 +1,204 @@
package main
import (
"errors"
"log/slog"
"github.com/sbondCo/Watcharr/arr"
)
type ArrSettings struct {
Name string `json:"name,omitempty"`
Host string `json:"host,omitempty"`
Key string `json:"key,omitempty"`
}
type SonarrSettings struct {
ArrSettings
QualityProfile int `json:"qualityProfile,omitempty"`
RootFolder int `json:"rootFolder,omitempty"`
LanguageProfile int `json:"languageProfile,omitempty"`
AutomaticSearch bool `json:"automaticSearch"`
// TODO eventually separate profiles and root for anime content (i can see diff language profile being useful)
}
func (s *SonarrSettings) safe() SonarrSettings {
s.Key = ""
return *s
}
type RadarrSettings struct {
ArrSettings
QualityProfile int `json:"qualityProfile,omitempty"`
RootFolder int `json:"rootFolder,omitempty"`
AutomaticSearch bool `json:"automaticSearch"`
}
func (s *RadarrSettings) safe() RadarrSettings {
s.Key = ""
return *s
}
type ArrTestParams struct {
Host string `json:"host,omitempty"`
Key string `json:"key,omitempty"`
}
type SonarrTestResponse struct {
QualityProfiles []arr.QualityProfile `json:"qualityProfiles"`
RootFolders []arr.RootFolder `json:"rootFolders"`
LanguageProfiles []arr.LanguageProfile `json:"languageProfiles"`
}
type RadarrTestResponse struct {
QualityProfiles []arr.QualityProfile `json:"qualityProfiles"`
RootFolders []arr.RootFolder `json:"rootFolders"`
LanguageProfiles []arr.LanguageProfile `json:"languageProfiles"`
}
func testSonarr(p ArrTestParams) (SonarrTestResponse, error) {
sonarr := arr.New(arr.SONARR, &p.Host, &p.Key)
qps, err := sonarr.GetQualityProfiles()
if err != nil {
slog.Error("testSonarr failed to get quality profiles!", "error", err)
return SonarrTestResponse{}, errors.New("failed to get quality profiles")
}
rfs, err := sonarr.GetRootFolders()
if err != nil {
slog.Error("testSonarr failed to get root folders!", "error", err)
return SonarrTestResponse{}, errors.New("failed to get root folders")
}
lps, err := sonarr.GetLangaugeProfiles()
if err != nil {
slog.Error("testSonarr failed to get language profiles!", "error", err)
return SonarrTestResponse{}, errors.New("failed to get language profiles")
}
return SonarrTestResponse{QualityProfiles: qps, RootFolders: rfs, LanguageProfiles: lps}, nil
}
func testRadarr(p ArrTestParams) (RadarrTestResponse, error) {
radarr := arr.New(arr.RADARR, &p.Host, &p.Key)
qps, err := radarr.GetQualityProfiles()
if err != nil {
slog.Error("testRadarr failed to get quality profiles!", "error", err)
return RadarrTestResponse{}, errors.New("failed to get quality profiles")
}
rfs, err := radarr.GetRootFolders()
if err != nil {
slog.Error("testRadarr failed to get root folders!", "error", err)
return RadarrTestResponse{}, errors.New("failed to get root folders")
}
return RadarrTestResponse{QualityProfiles: qps, RootFolders: rfs}, nil
}
// TODO any way to simplify (deduplicate/reuse) these methods (and the whole file tbh) would be very good
// Add sonarr server to config
func addSonarr(s SonarrSettings) error {
for _, v := range Config.SONARR {
if v.Name == s.Name {
// Server exists with this name...
return errors.New("server with that name already exists")
}
}
Config.SONARR = append(Config.SONARR, s)
writeConfig()
return nil
}
// Edit sonarr server in config
func editSonarr(s SonarrSettings) error {
for i, v := range Config.SONARR {
if v.Name == s.Name {
Config.SONARR[i] = s
writeConfig()
return nil
}
}
return errors.New("can't edit server that does not exist")
}
func rmSonarr(name string) error {
for i, v := range Config.SONARR {
if v.Name == name {
Config.SONARR = append(Config.SONARR[:i], Config.SONARR[i+1:]...)
writeConfig()
return nil
}
}
return errors.New("can't remove a server that does not exist")
}
func getSonarr(name string) (SonarrSettings, error) {
for i, v := range Config.SONARR {
if v.Name == name {
return Config.SONARR[i], nil
}
}
return SonarrSettings{}, errors.New("server not found")
}
// Get list of sonarr servers without api keys.
// Regular users with access to adding to sonarr will request this.
func getSonarrsSafe() []SonarrSettings {
s := []SonarrSettings{}
for _, v := range Config.SONARR {
s = append(s, v.safe())
}
return s
}
// Add radarr server to config
func addRadarr(s RadarrSettings) error {
for _, v := range Config.RADARR {
if v.Name == s.Name {
// Server exists with this name...
return errors.New("server with that name already exists")
}
}
Config.RADARR = append(Config.RADARR, s)
writeConfig()
return nil
}
// Edit radarr server in config
func editRadarr(s RadarrSettings) error {
for i, v := range Config.RADARR {
if v.Name == s.Name {
Config.RADARR[i] = s
writeConfig()
return nil
}
}
return errors.New("can't edit server that does not exist")
}
func rmRadarr(name string) error {
for i, v := range Config.RADARR {
if v.Name == name {
Config.RADARR = append(Config.RADARR[:i], Config.RADARR[i+1:]...)
writeConfig()
return nil
}
}
return errors.New("can't remove a server that does not exist")
}
func getRadarr(name string) (RadarrSettings, error) {
for i, v := range Config.RADARR {
if v.Name == name {
return Config.RADARR[i], nil
}
}
return RadarrSettings{}, errors.New("server not found")
}
// Get list of radarr servers without api keys.
// Regular users with access to adding to radarr will request this.
func getRadarrsSafe() []RadarrSettings {
s := []RadarrSettings{}
for _, v := range Config.RADARR {
s = append(s, v.safe())
}
return s
}
+235
View File
@@ -0,0 +1,235 @@
// Sonarr and Radarr
package arr
import (
"bytes"
"encoding/json"
"errors"
"io"
"log/slog"
"net/http"
"net/url"
)
type ArrType string
var (
SONARR ArrType = "SONARR"
RADARR ArrType = "RADARR"
)
type Arr struct {
// Type of Arr we want to use.
// Each servars api might differ, so this will
// allow us to make those changes when needed.
Type ArrType
// Hostname to the Arr sever
Host *string
// Api key for the Arr server.
Key *string
}
type ArrRequest struct {
ServerName string `json:"serverName"`
QualityProfile int `json:"qualityProfile"` // id
RootFolder string `json:"rootFolder"` // path
AutomaticSearch bool `json:"automaticSearch"`
Title string `json:"title"` // content name
Year int `json:"year"` // content year
}
type SonarrRequest struct {
ArrRequest
TVDBID int `json:"tvdbId"`
LanguageProfile int `json:"languageProfile"` // id
SeriesType string `json:"seriesType"`
Seasons []SonarrSeasons `json:"seasons"`
}
type RadarrRequest struct {
ArrRequest
TMDBID int `json:"tmdbId"`
}
type SonarrSeasons struct {
SeasonNumber int `json:"seasonNumber"`
Monitored bool `json:"monitored"`
}
func New(t ArrType, host *string, key *string) *Arr {
return &Arr{
Type: t,
Host: host,
Key: key,
}
}
func (a *Arr) GetQualityProfiles() ([]QualityProfile, error) {
slog.Info("GetQualityProfiles", "type", a.Type, "host", *a.Host, "key", *a.Key)
var resp []QualityProfile
err := request(*a.Host, "/qualityprofile", map[string]string{"apikey": *a.Key}, &resp)
if err != nil {
slog.Error("GetQualityProfiles request failed", "service", a.Type, "error", err)
return []QualityProfile{}, errors.New("request to service failed")
}
return resp, nil
}
func (a *Arr) GetRootFolders() ([]RootFolder, error) {
slog.Info("GetRootFolders", "type", a.Type, "host", *a.Host, "key", *a.Key)
var resp []RootFolder
err := request(*a.Host, "/rootfolder", map[string]string{"apikey": *a.Key}, &resp)
if err != nil {
slog.Error("GetRootFolders request failed", "service", a.Type, "error", err)
return []RootFolder{}, errors.New("request to service failed")
}
return resp, nil
}
func (a *Arr) GetLangaugeProfiles() ([]LanguageProfile, error) {
slog.Info("GetLangaugeProfiles", "type", a.Type, "host", *a.Host, "key", *a.Key)
var resp []LanguageProfile
// languageprofile supposedly deprecated.. but new language endpoint doesnt seem to work.. note probs to switch soon
err := request(*a.Host, "/languageprofile", map[string]string{"apikey": *a.Key}, &resp)
if err != nil {
slog.Error("GetLangaugeProfiles request failed", "service", a.Type, "error", err)
return []LanguageProfile{}, errors.New("request to service failed")
}
return resp, nil
}
// TODO do title! and year?
func (a *Arr) BuildAddShowBody(r SonarrRequest) map[string]interface{} {
req := map[string]interface{}{
"title": r.Title,
"year": r.Year,
"qualityProfileId": r.QualityProfile,
"languageProfileId": r.LanguageProfile,
"seasonFolder": true,
"monitored": true,
"tvdbId": r.TVDBID,
"seriesType": r.SeriesType,
"seasons": r.Seasons,
"addOptions": map[string]interface{}{
"ignoreEpisodesWithFiles": true,
"searchForMissingEpisodes": r.AutomaticSearch,
},
"rootFolderPath": r.RootFolder,
}
return req
}
func (a *Arr) BuildAddMovieBody(r RadarrRequest) map[string]interface{} {
req := map[string]interface{}{
"title": r.Title,
"year": r.Year,
"qualityProfileId": r.QualityProfile,
"monitored": true,
"tmdbId": r.TMDBID,
"addOptions": map[string]interface{}{
"searchForMovie": r.AutomaticSearch,
},
"rootFolderPath": r.RootFolder,
}
return req
}
func (a *Arr) AddContent(b map[string]interface{}) error {
ep := "series"
if a.Type == RADARR {
ep = "movie"
}
slog.Debug("AddContent", "type", ep, "body", b)
var resp interface{}
err := requestPost(*a.Host, "/"+ep, *a.Key, b, &resp)
if err != nil {
slog.Error("AddContent request failed", "service", a.Type, "error", err)
return errors.New("request to service failed")
}
return nil
}
func request(host string, ep string, p map[string]string, resp interface{}) error {
slog.Debug("tmdbAPIRequest", "endpoint", ep, "params", p)
base, err := url.Parse(host)
if err != nil {
return errors.New("failed to parse api uri")
}
// Path params
base.Path += "/api/v3" + ep
// Query params
params := url.Values{}
for k, v := range p {
params.Add(k, v)
}
// Add params to url
base.RawQuery = params.Encode()
// Run get request
res, err := http.Get(base.String())
if err != nil {
return err
}
body, err := io.ReadAll(res.Body)
res.Body.Close()
if err != nil {
return err
}
if res.StatusCode != 200 {
slog.Error("arr non 200 status code:", "status_code", res.StatusCode)
return errors.New(string(body))
}
// slog.Info("", "body", body)
err = json.Unmarshal([]byte(body), &resp)
if err != nil {
return err
}
return nil
}
func requestPost(host string, ep string, key string, p map[string]interface{}, resp interface{}) error {
base, err := url.Parse(host)
if err != nil {
return errors.New("failed to parse api uri")
}
// Path params
base.Path += "/api/v3" + ep
var res *http.Response
// Query params
params := url.Values{}
params.Add("apikey", key)
// Add params to url
base.RawQuery = params.Encode()
jsonp, err := json.Marshal(p)
if err != nil {
return err
}
res, err = http.Post(base.String(), "application/json", bytes.NewBuffer(jsonp))
if err != nil {
return err
}
body, err := io.ReadAll(res.Body)
res.Body.Close()
if err != nil {
return err
}
if !(res.StatusCode >= 200 && res.StatusCode <= 299) {
slog.Error("arr non 2xx status code:", "status_code", res.StatusCode)
return errors.New(string(body))
}
err = json.Unmarshal([]byte(body), &resp)
if err != nil {
return err
}
return nil
}
+45
View File
@@ -0,0 +1,45 @@
package arr
type QualityProfile struct {
Name string `json:"name"`
UpgradeAllowed bool `json:"upgradeAllowed"`
Cutoff int `json:"cutoff"`
Items []struct {
Quality struct {
ID int `json:"id"`
Name string `json:"name"`
Source string `json:"source"`
Resolution int `json:"resolution"`
} `json:"quality,omitempty"`
Items []any `json:"items"`
Allowed bool `json:"allowed"`
Name string `json:"name,omitempty"`
ID int `json:"id,omitempty"`
} `json:"items"`
ID int `json:"id"`
}
type RootFolder struct {
Path string `json:"path"`
Accessible bool `json:"accessible"`
FreeSpace int64 `json:"freeSpace"`
UnmappedFolders []any `json:"unmappedFolders"`
ID int `json:"id"`
}
type LanguageProfile struct {
Name string `json:"name"`
UpgradeAllowed bool `json:"upgradeAllowed"`
Cutoff struct {
ID int `json:"id"`
Name string `json:"name"`
} `json:"cutoff"`
Languages []struct {
Language struct {
ID int `json:"id"`
Name string `json:"name"`
} `json:"language"`
Allowed bool `json:"allowed"`
} `json:"languages"`
ID int `json:"id"`
}
+376 -41
View File
@@ -10,9 +10,11 @@ import (
"fmt"
"io"
"log"
"log/slog"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
@@ -27,6 +29,17 @@ type UserType uint8
var (
// Assume watcharr user if none of these...
JELLYFIN_USER UserType = 1
PLEX_USER UserType = 2
)
// User Perms
// iota auto increments for us so when adding new
// perms, add to bottom as to not change other perm
// values.
const (
PERM_NONE int = 1 << iota
PERM_ADMIN
PERM_REQUEST_CONTENT
)
// uniqueIndex applied between Username and UserType, so same usernames can exist, but only with different types.
@@ -35,12 +48,82 @@ type User struct {
GormModel
Username string `gorm:"uniqueIndex:usr_name_to_type;not null" json:"username" binding:"required"`
Password string `gorm:"not null" json:"password" binding:"required"`
AvatarID uint `json:"-"`
Avatar Image `json:"avatar"`
Bio string `json:"bio"`
// The type of user/which auth service they originate from.
// Empty if from Watcharr, or the name of the service (eg. jellyfin)
Type UserType `gorm:"uniqueIndex:usr_name_to_type;not null;default:0" json:"type"`
// ID of user from the third party service, this will be used purely for lookup of user at signin.
ThirdPartyID string `json:"-"`
// Auth token from third party (jellyfin)
ThirdPartyAuth string `json:"-"`
// Users third party integrations (minus jellyfin for now)
UserServices []UserServices `json:"-"`
Watched []Watched
Permissions int `gorm:"default:1" json:"-"`
// All user settings cols, in another struct for reusability
UserSettings
}
func (u *User) GetSafe() PublicUser {
return PublicUser{
ID: u.ID,
Username: u.Username,
Avatar: u.Avatar,
Bio: u.Bio,
}
}
// This struct uses pointer to the values, so in update user settings,
// we can tell which setting is being updated (if not nil..).
type UserSettings struct {
// Is profile private
Private *bool `gorm:"default:false" json:"private"`
// Are watched list content thoughts public (profile must also be public is false)
PrivateThoughts *bool `gorm:"default:false" json:"privateThoughts"`
// If ui 'spoilers' should be shown
HideSpoilers *bool `gorm:"default:false" json:"hideSpoilers"`
// If user wants previously watched items to show in 'Finished' filter,
// even if the watched item state has since been changed.
// Also if user wants to show in watched stats.
IncludePreviouslyWatched *bool `gorm:"default:false" json:"includePreviouslyWatched"`
}
// Holds third party service auth tokens for users.
// Each service may use the fields in their own way.
// Unique index applied between service name and clientID
// to ensure no duplicates (no need to apply it against
// user_id, no accounts should share an integration).
//
// Plex:
// - AuthToken : Used for requests against plex.tv
// - AuthToken2 : Used for requests against home plex server.
type UserServices struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
// Service/integration name
Name string `gorm:"uniqueIndex:svc_name_to_cltid;not null;" json:"-"`
// The users id on the third party service
ClientID string `gorm:"uniqueIndex:svc_name_to_cltid;not null;" json:"-"`
AuthToken string `gorm:"not null;" json:"-"`
// Second auth token, generic name so future services can use it without extra confusion.
// Ex: We require a second auth token for use with our local server for Plex.
AuthToken2 string `json:"-"`
UserID uint `gorm:"not null;" json:"-"`
}
// We use a separate struct for registration to avoid confusion
// and possible accidents where we allow a user to pass in a
// property from the main User struct that shouldn't be allowed.
type UserRegisterRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type UseAdminTokenRequest struct {
Token string `json:"token" binding:"required"`
}
type JellyfinAuth struct {
@@ -53,12 +136,19 @@ type JellyfinAuthResponse struct {
ID string `json:"Id"`
Name string `json:"Name"`
} `json:"User"`
AccessToken string `json:"AccessToken"`
}
type AuthResponse struct {
Token string `json:"token"`
}
type AvailableAuthProvidersResponse struct {
AvailableAuthProviders []string `json:"available"`
SignupEnabled bool `json:"signupEnabled"`
IsInSetup bool `json:"isInSetup"`
}
type ArgonParams struct {
memory uint32
iterations uint32
@@ -67,20 +157,37 @@ type ArgonParams struct {
keyLength uint32
}
func GetPassArgonParams() *ArgonParams {
return &ArgonParams{
memory: 64 * 1024,
iterations: 3,
parallelism: 2,
saltLength: 16,
keyLength: 32,
}
}
type TokenClaims struct {
UserID uint `json:"userId"`
Username string `json:"username"`
UserID uint `json:"userId"`
Username string `json:"username"`
Type UserType `json:"type"`
jwt.RegisteredClaims
}
type UserPasswordUpdateRequest struct {
OldPassword string `json:"oldPassword" binding:"required"`
NewPassword string `json:"newPassword" binding:"required"`
}
// Auth middleware
func AuthRequired() gin.HandlerFunc {
// If db is passed, extra user info from the database will be fetched.
func AuthRequired(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
println("AuthRequired middleware hit")
slog.Debug("AuthRequired middleware hit")
atoken := c.GetHeader("Authorization")
// Make sure auth header isn't empty
if atoken == "" {
println("Returning 401, Authorization header not provided")
slog.Warn("Returning 401, Authorization header not provided")
c.AbortWithStatus(401)
return
}
@@ -88,26 +195,73 @@ func AuthRequired() gin.HandlerFunc {
token, err := jwt.ParseWithClaims(atoken, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(os.Getenv("JWT_SECRET")), nil
})
if err != nil {
slog.Error("AuthRequired failed to parse token", "error", err)
c.AbortWithStatus(401)
return
}
// If token is valid, go to next handler
if claims, ok := token.Claims.(*TokenClaims); ok && token.Valid {
println("Token valid", claims.Username)
// Check if token issuedAt is from before `timeOfNewLoginRequired`.
// Basically just so we can logout old tokens and force relogin...
// since new changes require the user login again.
timeOfNewLoginRequired, _ := time.Parse(time.RFC822, "18 Aug 23 20:30 UTC")
if claims.IssuedAt.Before(timeOfNewLoginRequired) {
slog.Info("Token is from before timeOfNewLoginRequired.. returning 401", "token_issued_at", claims.IssuedAt, "time_of_new_login_required", timeOfNewLoginRequired)
c.AbortWithStatus(401)
return
}
slog.Debug("Token is valid", "claims", claims)
c.Set("userId", claims.UserID)
c.Set("userType", claims.Type)
// If db passed, get extra user info and set as variables in req context
if db != nil {
slog.Debug("AuthRequired: db passed.. getting extra user info")
dbUser := new(User)
res := db.Where("id = ?", claims.UserID).Take(&dbUser)
if res.Error != nil {
slog.Error("AuthRequired: Failed to select user from database", "error", res.Error)
c.AbortWithStatus(401)
return
}
slog.Debug("AuthRequired: fetched extra user info. Setting vars.", "userThirdPartyId", dbUser.ThirdPartyID, "userThirdPartyAuth", "lol this is censored dude")
c.Set("userThirdPartyId", dbUser.ThirdPartyID)
c.Set("userThirdPartyAuth", dbUser.ThirdPartyAuth)
c.Set("username", dbUser.Username)
c.Set("userPermissions", dbUser.Permissions)
}
c.Next()
} else {
fmt.Println(err)
slog.Error("Token is **not** valid")
c.AbortWithStatus(401)
return
}
}
}
func register(user *User, db *gorm.DB) (AuthResponse, error) {
println("Registering", user.Username)
hash, err := hashPassword(user.Password, &ArgonParams{
memory: 64 * 1024,
iterations: 3,
parallelism: 2,
saltLength: 16,
keyLength: 32,
})
// Admin only middleware (use after AuthRequired with extra info!)
func AdminRequired() gin.HandlerFunc {
return func(c *gin.Context) {
userId := c.GetUint("userId")
perms := c.GetInt("userPermissions")
if hasPermission(perms, PERM_ADMIN) {
slog.Debug("AdminRequired: User has permission to access admin only route", "user_id", userId)
c.Next()
return
}
slog.Info("AdminRequired: User denied permission to access admin only route", "user_id", userId)
c.AbortWithStatus(401)
}
}
func register(ur *UserRegisterRequest, initialPerm int, db *gorm.DB) (AuthResponse, error) {
if !Config.SIGNUP_ENABLED {
slog.Warn("Register called, but signing up is disabled.")
return AuthResponse{}, errors.New("registering is disabled")
}
var user User = User{Username: ur.Username, Password: ur.Password}
slog.Info("A user is registering", "username", user.Username)
hash, err := hashPassword(user.Password, GetPassArgonParams())
if err != nil {
log.Fatal(err)
}
@@ -115,100 +269,121 @@ func register(user *User, db *gorm.DB) (AuthResponse, error) {
// Update user obj to replace the plaintext pass with hash
user.Password = hash
// Update user permissions if an initial perm is passed in (1 is default)
if initialPerm != 0 && initialPerm != PERM_NONE {
slog.Info("User being registered has been given extra initial permissions", "initial_perm", initialPerm)
user.Permissions = initialPerm
}
res := db.Create(&user)
if res.Error != nil {
// If error is because unique contraint failed.. user already exists
if strings.Contains(res.Error.Error(), "UNIQUE") {
println("Unique contraint fail:", res.Error.Error())
if res.Error == gorm.ErrDuplicatedKey {
slog.Error("Registration failed", "error", res.Error.Error(), "error_pretty", "User already exists")
return AuthResponse{}, errors.New("User already exists")
}
panic(err)
slog.Error("Registration failed", "error", err, "error_pretty", "Watcharr does not know why this failed, assume database operation failed")
return AuthResponse{}, errors.New("unknown error")
}
// Gorm fills our user obj with the ID from db after insert,
// just ensure it actually has.
if user.ID == 0 {
fmt.Println("user.ID not filled out after registration", user.ID)
slog.Error("user.ID not filled out after registration", "userId", user.ID)
return AuthResponse{}, errors.New("failed to get user id, try login")
}
token, err := signJWT(user)
token, err := signJWT(&user)
if err != nil {
fmt.Println("Failed to sign new jwt:", err)
slog.Error("Registration: Failed to sign new jwt", "error", err)
return AuthResponse{}, errors.New("failed to get auth token")
}
return AuthResponse{Token: token}, nil
}
func registerFirstUser(user *UserRegisterRequest, db *gorm.DB) (AuthResponse, error) {
// Ensure no users exist
var userCount int64
uresp := db.Model(&User{}).Count(&userCount)
if uresp.Error != nil {
slog.Error("registerFirstUser: User count query failed!", "error", uresp.Error)
return AuthResponse{}, errors.New("failed to query db for a count of users")
}
if userCount != 0 {
slog.Warn("registerFirstUser: registered users already exist.")
return AuthResponse{}, errors.New("first user already registered")
}
slog.Info("Registering first user.")
return register(user, PERM_ADMIN, db)
}
func login(user *User, db *gorm.DB) (AuthResponse, error) {
fmt.Println("Logging in", user.Username)
slog.Debug("A User Is Logging In", "username", user.Username)
dbUser := new(User)
res := db.Where("username = ? AND (type IS NULL OR type = 0)", user.Username).Take(&dbUser)
if res.Error != nil {
fmt.Println("Failed to select user from database for login:", res.Error)
slog.Error("Failed to select user from database for login", "error", res.Error)
return AuthResponse{}, errors.New("User does not exist")
}
match, err := compareHash(user.Password, dbUser.Password)
if err != nil {
fmt.Println("Failed to compare pass to hash for login:", err)
slog.Error("Failed to compare pass to hash for login", "error", err)
return AuthResponse{}, errors.New("failed to login")
}
if !match {
fmt.Println("User failed to provide correct password for login:", match)
slog.Error("User failed to provide correct password for login", "hash_matched", match)
return AuthResponse{}, errors.New("incorrect details")
}
token, err := signJWT(dbUser)
if err != nil {
fmt.Println("Failed to sign new jwt:", err)
slog.Error("Failed to sign new jwt", "error", err)
return AuthResponse{}, errors.New("failed to get auth token")
}
return AuthResponse{Token: token}, nil
}
func loginJellyfin(user *User, db *gorm.DB) (AuthResponse, error) {
jellyfinHost := os.Getenv("JELLYFIN_HOST")
if jellyfinHost == "" {
println("Request made to login via Jellyfin, but JELLYFIN_HOST environment variable is not set.")
if Config.JELLYFIN_HOST == "" {
slog.Error("Request made to login via Jellyfin, but JELLYFIN_HOST has not been configured.")
return AuthResponse{}, errors.New("jellyfin login not enabled")
}
base, err := url.Parse(jellyfinHost + "/Users/AuthenticateByName")
base, err := url.Parse(Config.JELLYFIN_HOST + "/Users/AuthenticateByName")
if err != nil {
println("Failed to parse AuthenticateByName api endpoint url:", err.Error())
slog.Error("Failed to parse AuthenticateByName api endpoint url", "error", err.Error())
return AuthResponse{}, errors.New("failed to parse api uri")
}
// Marshall struct as json
usrJSON, err := json.Marshal(JellyfinAuth{Username: user.Username, Pw: user.Password})
if err != nil {
println("Error marshalling JellyfinAuth JSON", err.Error())
slog.Error("Error marshalling JellyfinAuth JSON", "error", err.Error())
return AuthResponse{}, errors.New("failed to marshal json")
}
// Run auth request
// res, err := http.Post(base.String(), "application/json", bytes.NewBuffer(usrJSON))
client := &http.Client{}
req, err := http.NewRequest("POST", base.String(), bytes.NewBuffer(usrJSON))
if err != nil {
println("creating request to jellyfin for auth failed:", err)
slog.Error("Creating request to jellyfin for auth failed", "error", err)
return AuthResponse{}, errors.New("request failed")
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("X-Emby-Authorization", "MediaBrowser Client=\"Watcharr\", Device=\"HTTP\", DeviceId=\"WatcharrFor"+user.Username+"\", Version=\"10.8.0\"")
res, err := client.Do(req)
if err != nil {
println("making request to jellyfin for auth failed:", err)
slog.Error("making request to jellyfin for auth failed", "error", err)
return AuthResponse{}, errors.New("request failed")
}
body, err := io.ReadAll(res.Body)
res.Body.Close()
if err != nil {
println("Error reading response", err.Error())
slog.Error("Error reading jellyfin auth response", "error", err.Error())
return AuthResponse{}, err
}
if res.StatusCode != 200 {
println("Jellyfin auth non 200 status code:", res.StatusCode, string(body))
slog.Error("Jellyfin auth non 200 status code", "status_code", res.StatusCode, "error", string(body))
return AuthResponse{}, errors.New("incorrect details")
}
// Process auth response
@@ -222,38 +397,155 @@ func loginJellyfin(user *User, db *gorm.DB) (AuthResponse, error) {
}
dbUser := new(User)
dbRes := db.Where("third_party_id = ?", resp.User.ID).Take(&dbUser)
dbRes := db.Where("third_party_id = ? AND type = ?", resp.User.ID, JELLYFIN_USER).Take(&dbUser)
if dbRes.Error != nil {
if errors.Is(dbRes.Error, gorm.ErrRecordNotFound) {
// Record not found, so we should create the user
// dbUser will be empty, so we can just reuse it for this purpose.
dbUser.ThirdPartyID = resp.User.ID
dbUser.ThirdPartyAuth = resp.AccessToken
dbUser.Username = resp.User.Name
dbUser.Type = JELLYFIN_USER
dbRes = db.Create(&dbUser)
if dbRes.Error != nil {
println("Failed to create new user in db from jellyfin response:", err.Error())
slog.Error("Failed to create new user in db from jellyfin response", "error", dbRes.Error)
return AuthResponse{}, errors.New("failed to create new user from jellyfin")
}
} else {
return AuthResponse{}, errors.New("error locating user in db")
}
}
// If user exists.. update their access token in db
if resp.AccessToken != "" {
slog.Debug("Jellyfin user login - updating user with new access token")
dbUser.ThirdPartyAuth = resp.AccessToken
db.Save(&dbUser)
}
token, err := signJWT(dbUser)
if err != nil {
fmt.Println("Failed to sign new jwt:", err)
slog.Error("Failed to sign new (jellyfin login) jwt", "error", err)
return AuthResponse{}, errors.New("failed to get auth token")
}
return AuthResponse{Token: token}, nil
}
// Login via Plex.
func loginPlex(lr *PlexLoginRequest, db *gorm.DB) (AuthResponse, error) {
if Config.PLEX_HOST == "" || Config.PLEX_MACHINE_ID == "" {
slog.Error("Request made to login via Plex, but Plex authentication is disabled")
return AuthResponse{}, errors.New("plex login not enabled")
}
slog.Debug("A Plex User Is Logging In")
account, err := fetchPlexAccountFromToken(lr.AuthToken)
if err != nil {
slog.Error("loginPlex: Could not fetch Plex account", "error", err)
return AuthResponse{}, errors.New("could not fetch plex acount")
}
if account.Username == "" || account.Id == 0 {
slog.Error("loginPlex: Username or id missing from account response:", "username", account.Username, "id", account.Id)
return AuthResponse{}, errors.New("data is missing from the plex account response")
}
// Get users auth token against our home plex server.
// If no auth token, assume they don't have access to our plex server.
homeAuthToken, err := getPlexHomeServerAuthToken(lr.AuthToken, lr.ClientIdentifier)
if err != nil || homeAuthToken == "" {
slog.Error("loginPlex: Failed to get home server auth token for user! If not because the request failed, then ensure the user has access to our home servers library.", "error", err)
return AuthResponse{}, errors.New("failed to verify plex access")
}
dbUser := new(User)
userIdQ := db.Select("user_id").Where("name = ? AND client_id = ?", "plex", account.Id).Table("user_services")
dbRes := db.Where("type = ?", PLEX_USER).Where("id = (?)", userIdQ).Preload("UserServices").Take(&dbUser)
if dbRes.Error != nil {
if errors.Is(dbRes.Error, gorm.ErrRecordNotFound) {
slog.Debug("loginPlex: New plex user attempted login.. creating Watcharr account now.")
dbUser.Username = account.Username
dbUser.Type = PLEX_USER
dbUser.UserServices = append(dbUser.UserServices, UserServices{
Name: "plex",
ClientID: strconv.FormatUint(account.Id, 10),
AuthToken: lr.AuthToken,
AuthToken2: homeAuthToken,
})
dbRes = db.Create(&dbUser)
if dbRes.Error != nil {
slog.Error("loginPlex: Failed to create new user in db from plex response", "error", dbRes.Error)
return AuthResponse{}, errors.New("failed to create new user from plex")
}
} else {
slog.Error("loginPlex: Failed to select user from database for login", "error", dbRes.Error)
return AuthResponse{}, errors.New("failed to locate user")
}
} else {
// If user exists.. update their access tokens in db
for i, v := range dbUser.UserServices {
if v.Name == "plex" {
slog.Info("loginPlex: Found plex user service.. attemping to update")
dbUser.UserServices[i].AuthToken = lr.AuthToken
if homeAuthToken != "" {
dbUser.UserServices[i].AuthToken2 = homeAuthToken
}
break
}
}
db.Save(&dbUser.UserServices)
}
token, err := signJWT(dbUser)
if err != nil {
slog.Error("loginPlex: Failed to sign new jwt", "error", err)
return AuthResponse{}, errors.New("failed to get auth token")
}
return AuthResponse{Token: token}, nil
}
func useAdminToken(req *UseAdminTokenRequest, db *gorm.DB, userId uint) error {
var dbToken Token
resp := db.Where("value = ?", req.Token).Take(&dbToken)
if resp.Error != nil {
slog.Info("useAdminToken failed", "error", "token not found in db")
return errors.New("invalid token")
}
if dbToken.Type != TOKENTYPE_ADMIN {
slog.Info("useAdminToken failed", "error", "token is of wrong type", "type_wanted", TOKENTYPE_ADMIN, "type_actual", dbToken.Type)
return errors.New("invalid token")
}
dur := time.Since(dbToken.CreatedAt)
if dur > tokenMaxAge {
slog.Info("useAdminToken failed", "error", "token in db has expired")
return errors.New("invalid token")
}
if dbToken.UserID != userId {
slog.Info("useAdminToken failed", "error", "token in db is not for this user")
return errors.New("invalid token")
}
// Token is valid and for current user.. give user admin.
// Incase removing the token after used fails, this is in a transaction so user wont be admin.
err := db.Transaction(func(tx *gorm.DB) error {
// Give user admin
if err := tx.Model(&User{}).Where("id = ?", userId).Update("permissions", PERM_ADMIN).Error; err != nil {
return err
}
// Delete used token
if err := tx.Where("value = ?", req.Token).Delete(&Token{}).Error; err != nil {
return err
}
// commit transaction if no errors
return nil
})
if err != nil {
slog.Info("useAdminToken failed", "error", err, "error_pretty", "using token transaction failed")
return errors.New("failed to use token")
}
return nil
}
func signJWT(user *User) (token string, err error) {
// Create new jwt with claim data
jwt := jwt.NewWithClaims(jwt.SigningMethodHS256, TokenClaims{
user.ID,
user.Username,
user.Type,
jwt.RegisteredClaims{
// ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
@@ -348,3 +640,46 @@ func decodeHash(encodedHash string) (p *ArgonParams, salt, hash []byte, err erro
return p, salt, hash, nil
}
func hasPermission(perms int, reqPerm int) bool {
// Admins have permission for everything.
if perms&PERM_ADMIN == PERM_ADMIN {
return true
}
return (perms & reqPerm) == reqPerm
}
func userChangePassword(db *gorm.DB, pwds UserPasswordUpdateRequest, userId uint) error {
slog.Debug("userChangePassword request running", "user_id", userId)
user := new(User)
res := db.Where("id = ?", userId).Select("password").Take(&user)
if res.Error != nil {
slog.Error("userChangePassword failed - failed to retrieve user from database", "user_id", userId, "error", res.Error)
return errors.New("failed to retrieve user")
}
slog.Debug("userChangePassword user found", "user_id", userId)
match, err := compareHash(pwds.OldPassword, user.Password)
if err != nil {
slog.Error("userChangePassword failed - failed to compare passwords", "user_id", userId, "error", err)
return errors.New("failed to compare passwords")
}
if !match {
slog.Error("userChangePassword failed - current password hash doesn't match password hash in database", "user_id", userId, "error", err)
return errors.New("current password provided doesn't match password in database")
}
slog.Debug("userChangePassword hash for current password matches hash in the database", "user_id", userId)
slog.Debug("userChangePassword hashing new password", "user_id", userId)
hash, err := hashPassword(pwds.NewPassword, GetPassArgonParams())
if err != nil {
slog.Error("userChangePassword failed - failed to hash new password", "user_id", userId, "error", err)
return errors.New("failed to hash new password")
}
slog.Debug("userChangePassword new password hashed", "user_id", userId)
if err := db.Model(&User{}).Where("id = ?", userId).Update("password", hash).Error; err != nil {
slog.Error("userChangePassword failed - failed to update password in database", "user_id", userId, "error", err)
return errors.New("failed to update password")
} else {
slog.Debug("userChangePassword password updated", "user_id", userId)
}
return nil
}
+266
View File
@@ -0,0 +1,266 @@
package main
import (
"encoding/json"
"errors"
"log"
"log/slog"
"os"
"time"
"github.com/sbondCo/Watcharr/game"
"gorm.io/gorm"
)
var DataPath = "./data"
type ServerConfig struct {
// Used to sign JWT tokens. Make sure to make
// it strong, just like a very long, complicated password.
JWT_SECRET string `json:",omitempty"`
// Optional: Point to your Jellyfin install
// to enable it as an auth provider.
JELLYFIN_HOST string `json:",omitempty"`
// Enable/disable signup functionality.
// Set to `false` to disable registering an account.
SIGNUP_ENABLED bool
// Optional: Provide your own TMDB API Key.
// If unprovided, the default Watcharr API key will be used.
TMDB_KEY string `json:",omitempty"`
// Optional: Point to Plex install to enable plex features.
PLEX_HOST string `json:",omitempty"`
// Optional: Machine identifier of your Plex server.
// This is used to ensure only users of your Plex server
// can use this Watcharr instance.
// Will be fetched automatically when PLEX_HOST is provided via web ui.
PLEX_MACHINE_ID string `json:",omitempty"`
SONARR []SonarrSettings `json:",omitempty"`
RADARR []RadarrSettings `json:",omitempty"`
TWITCH game.IGDB `json:",omitempty"`
// Enable/disable debug logging. Useful for when trying
// to figure out exactly what the server is doing at a point
// of failure.
// Set to `true` to enable.
DEBUG bool `json:",omitempty"`
}
// ServerConfig, but with JWT_SECRET removed from json.
// Used for returning to user from get config api request.
//
// Technically only admins will have access to that api route,
// but I feel more comfortable removing it anyways (+ this is
// not editable on frontend, so not needed).
func (c *ServerConfig) GetSafe() ServerConfig {
return ServerConfig{
SIGNUP_ENABLED: c.SIGNUP_ENABLED,
JELLYFIN_HOST: c.JELLYFIN_HOST,
TMDB_KEY: c.TMDB_KEY,
PLEX_HOST: c.PLEX_HOST,
PLEX_MACHINE_ID: c.PLEX_MACHINE_ID,
DEBUG: c.DEBUG,
SONARR: c.SONARR, // Dont act safe, this contains sonarr api key, needed for config
RADARR: c.RADARR, // Dont act safe, this contains radarr api key, needed for config
TWITCH: game.IGDB{
ClientID: c.TWITCH.ClientID,
ClientSecret: c.TWITCH.ClientSecret,
}, // Dont act safe, this contains twitch secrets, needed for config
}
}
var (
// Our server config.. `readConfig` will overwrite from watcharr.json cfg file.
Config = ServerConfig{}
)
// Read config file
// Calls generateConfig if file doesn't exist
func readConfig() error {
cfg, err := os.Open("./data/watcharr.json")
if err != nil {
if os.IsNotExist(err) {
slog.Info("Config file doesn't exist... generating.")
if err = generateConfig(); err == nil {
return nil
}
}
return err
}
defer cfg.Close()
jsonParser := json.NewDecoder(cfg)
if err = jsonParser.Decode(&Config); err != nil {
return err
}
initFromConfig()
return nil
}
// Ensure required config is provided
func initFromConfig() error {
if Config.JWT_SECRET == "" {
log.Fatal("JWT_SECRET missing from config!")
}
return nil
}
// Generate new barebones watcharr.json config file.
// Generates a JWT_SECRET and set default config.
func generateConfig() error {
key, err := generateString(64)
if err != nil {
return err
}
cfg := ServerConfig{
JWT_SECRET: key,
// Other defaults..
SIGNUP_ENABLED: true,
}
barej, err := json.MarshalIndent(cfg, "", "\t")
if err != nil {
return err
}
Config = cfg
return os.WriteFile("./data/watcharr.json", barej, 0755)
}
// Update server config property
func updateConfig(k string, v any) error {
slog.Debug("updateConfig", "k", k, "v", v)
if v == nil {
return errors.New("invalid value")
}
if k == "JELLYFIN_HOST" {
Config.JELLYFIN_HOST = v.(string)
} else if k == "SIGNUP_ENABLED" {
Config.SIGNUP_ENABLED = v.(bool)
} else if k == "TMDB_KEY" {
Config.TMDB_KEY = v.(string)
} else if k == "DEBUG" {
Config.DEBUG = v.(bool)
setLoggingLevel()
} else {
return errors.New("invalid setting")
}
return writeConfig()
}
// Write current Config to file
func writeConfig() error {
barej, err := json.MarshalIndent(Config, "", "\t")
if err != nil {
return err
}
return os.WriteFile("./data/watcharr.json", barej, 0755)
}
type ServerFeatures struct {
Sonarr bool `json:"sonarr"`
Radarr bool `json:"radarr"`
Games bool `json:"games"`
}
// Get enabled server functionality from Config.
// Mainly so the frontend can store this once and know
// which btns should be shown, etc.
func getEnabledFeatures(userPerms int) ServerFeatures {
var f ServerFeatures
if Config.TWITCH.ClientID != nil && Config.TWITCH.ClientSecret != nil {
f.Games = true
}
// https://github.com/sbondCo/Watcharr/issues/211
// Currently requesting permissions have not been setup, only admins for now.
if !hasPermission(userPerms, PERM_ADMIN) {
return f
}
if len(Config.SONARR) > 0 {
f.Sonarr = true
}
if len(Config.RADARR) > 0 {
f.Radarr = true
}
return f
}
func saveTwitchConfig(c game.IGDB) error {
// If existing client id and secret are same.. just return here
if (Config.TWITCH.ClientID != nil && c.ClientID != nil && Config.TWITCH.ClientSecret != nil && c.ClientSecret != nil) &&
*Config.TWITCH.ClientID == *c.ClientID && *Config.TWITCH.ClientSecret == *c.ClientSecret {
slog.Info("saveTwitchConfig: New ClientID and ClientSecret match old ClientID and ClientSecret.. ignoring request to update.")
return nil
}
// Update our config
Config.TWITCH.ClientID = c.ClientID
Config.TWITCH.ClientSecret = c.ClientSecret
Config.TWITCH.AccessToken = ""
Config.TWITCH.AccessTokenExpires = time.Time{}
// Try to init again
err := Config.TWITCH.Init()
if err != nil {
slog.Error("saveTwitchConfig failed to initialize TWITCH", "error", err)
return errors.New("initialization with credentials failed")
}
err = writeConfig()
if err != nil {
slog.Error("saveTwitchConfig failed to write config", "error", err)
return errors.New("failed to save config")
}
return nil
}
type ServerStats struct {
Users int64 `json:"users"`
PrivateUsers int64 `json:"privateUsers"`
WatchedMovies int64 `json:"watchedMovies"`
WatchedShows int64 `json:"watchedShows"`
WatchedSeasons int64 `json:"watchedSeasons"`
MostWatchedMovie Content `json:"mostWatchedMovie"`
MostWatchedShow Content `json:"mostWatchedShow"`
Activities int64 `json:"activities"`
}
// Collect and return server stats
// I cant sql so this the best yall gettin
func getServerStats(db *gorm.DB) ServerStats {
stats := ServerStats{}
resp := db.Model(&User{}).Count(&stats.Users).Where("private = 1").Count(&stats.PrivateUsers)
if resp.Error != nil {
slog.Error("getServerStats - Users query failed", "error", resp.Error)
}
resp = db.Model(&WatchedSeason{}).Count(&stats.WatchedSeasons)
if resp.Error != nil {
slog.Error("getServerStats - WatchedSeasons query failed", "error", resp.Error)
}
resp = db.Model(&Activity{}).Count(&stats.Activities)
if resp.Error != nil {
slog.Error("getServerStats - Activities query failed", "error", resp.Error)
}
resp = db.Joins("JOIN contents ON contents.id = watcheds.content_id AND contents.type = ?", "tv").Find(&Watched{}).Count(&stats.WatchedShows)
if resp.Error != nil {
slog.Error("getServerStats - WatchedShows query failed", "error", resp.Error)
}
resp = db.Joins("JOIN contents ON contents.id = watcheds.content_id AND contents.type = ?", "movie").Find(&Watched{}).Count(&stats.WatchedMovies)
if resp.Error != nil {
slog.Error("getServerStats - WatchedMovies query failed", "error", resp.Error)
}
var w Watched
resp = db.Model(&Watched{}).Select("content_id, COUNT(*) AS mag").Joins("JOIN contents ON contents.type = ? AND contents.id = watcheds.content_id", "tv").Group("content_id").Order("mag DESC").Preload("Content").First(&w)
if resp.Error != nil {
slog.Error("getServerStats - MostWatchedShow query failed", "error", resp.Error)
} else {
stats.MostWatchedShow = *w.Content
}
resp = db.Model(&Watched{}).Select("content_id, COUNT(*) AS mag").Joins("JOIN contents ON contents.type = ? AND contents.id = watcheds.content_id", "movie").Group("content_id").Order("mag DESC").Preload("Content").First(&w)
if resp.Error != nil {
slog.Error("getServerStats - MostWatchedMovie query failed", "error", resp.Error)
} else {
stats.MostWatchedMovie = *w.Content
}
return stats
}
+258 -17
View File
@@ -2,6 +2,12 @@ package main
import (
"errors"
"log/slog"
"path"
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type ContentType string
@@ -13,31 +19,200 @@ const (
// For storing cached content, so we can serve the basic local data for watched list to work
type Content struct {
ID int `json:"id" gorm:"primaryKey;autoIncrement"`
TmdbID int `json:"tmdbId" gorm:"uniqueIndex:contentidtotypeidx;not null"`
Title string `json:"title"`
PosterPath string `json:"poster_path"`
Overview string `json:"overview"`
Type ContentType `json:"type" gorm:"uniqueIndex:contentidtotypeidx;not null"`
ID int `json:"id" gorm:"primaryKey;autoIncrement"`
TmdbID int `json:"tmdbId" gorm:"uniqueIndex:contentidtotypeidx;not null"`
Title string `json:"title"`
PosterPath string `json:"poster_path"`
Overview string `json:"overview"`
Type ContentType `json:"type" gorm:"uniqueIndex:contentidtotypeidx;not null"`
ReleaseDate *time.Time `json:"release_date,omitempty"`
Popularity float32 `json:"popularity"`
VoteAverage float32 `json:"vote_average"`
VoteCount uint32 `json:"vote_count"`
ImdbID string `json:"imdb_id"`
Status string `json:"status"`
Budget uint32 `json:"budget"`
Revenue uint32 `json:"revenue"`
Runtime uint32 `json:"runtime"`
NumberOfEpisodes uint32 `json:"numberOfEpisodes"`
NumberOfSeasons uint32 `json:"numberOfSeasons"`
}
// onlyUpdate - If we should only update existing row if exists, or false to create/update if not exist.
func saveContent(db *gorm.DB, c *Content, onlyUpdate bool) error {
slog.Info("Saving content to db", "id", c.TmdbID, "title", c.Title)
if c.TmdbID == 0 || c.Title == "" || c.Type == "" {
slog.Error("saveContent: content missing id, title or type!", "id", c.TmdbID, "title", c.Title, "type", c.Type)
return errors.New("content missing id or title")
}
var res *gorm.DB
if onlyUpdate {
// We only want to update an existing row, if it exists.
res = db.Model(&Content{}).Where("type = ? AND tmdb_id = ?", c.Type, c.TmdbID).Updates(c)
if res.Error != nil {
slog.Error("saveContent: Error updating content in database", "error", res.Error.Error())
return errors.New("failed to update cached content in database")
}
} else {
// On conflict, update existing row with details incase any were updated/missing.
res = db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "tmdb_id"}, {Name: "type"}},
DoUpdates: clause.AssignmentColumns([]string{
"title",
"poster_path",
"overview",
"release_date",
"popularity",
"vote_average",
"vote_count",
"imdb_id",
"status",
"budget",
"revenue",
"runtime",
"number_of_episodes",
"number_of_seasons",
}),
}).Create(&c)
if res.Error != nil {
// Error if anything but unique contraint error
if res.Error != gorm.ErrDuplicatedKey {
slog.Error("saveContent: Error creating content in database", "error", res.Error.Error())
return errors.New("failed to cache content in database")
}
}
}
// If row created, download the image
if res.RowsAffected > 0 {
slog.Debug("saveContent: Downloading poster.")
err := download("https://image.tmdb.org/t/p/w500"+c.PosterPath, path.Join("./data/img", c.PosterPath))
if err != nil {
slog.Error("saveContent: Failed to download content image!", "error", err.Error())
}
}
return nil
}
func cacheContentTv(db *gorm.DB, content TMDBShowDetails, onlyUpdate bool) (Content, error) {
slog.Debug("cacheContentTv", "content", content)
var (
releaseDate time.Time
runtime uint32
)
var dateFormat = "2006-01-02"
releaseDate, err := time.Parse(dateFormat, content.FirstAirDate)
if err != nil {
slog.Error("Failed to parse tv release date", "error", err)
}
if len(content.EpisodeRunTime) > 0 {
runtime = uint32(content.EpisodeRunTime[0])
}
c := Content{
TmdbID: content.ID,
Title: content.Name,
Overview: content.Overview,
PosterPath: content.PosterPath,
Type: SHOW,
ReleaseDate: &releaseDate,
Popularity: content.Popularity,
VoteAverage: content.VoteAverage,
VoteCount: content.VoteCount,
Status: content.Status,
Runtime: runtime,
NumberOfEpisodes: content.NumberOfEpisodes,
NumberOfSeasons: content.NumberOfSeasons,
}
err = saveContent(db, &c, onlyUpdate)
if err != nil {
slog.Error("cacheContentTv: Failed to save content!", "error", err)
return Content{}, errors.New("failed to save content")
}
return c, nil
}
func cacheContentMovie(db *gorm.DB, content TMDBMovieDetails, onlyUpdate bool) (Content, error) {
var (
releaseDate time.Time
)
var dateFormat = "2006-01-02"
// Get details from movie/show response and fill out needed vars
releaseDate, err := time.Parse(dateFormat, content.ReleaseDate)
if err != nil {
slog.Error("Failed to parse movie release date", "error", err)
}
c := Content{
TmdbID: content.ID,
Title: content.Title,
Overview: content.Overview,
PosterPath: content.PosterPath,
Type: MOVIE,
ReleaseDate: &releaseDate,
Popularity: content.Popularity,
VoteAverage: content.VoteAverage,
VoteCount: content.VoteCount,
ImdbID: content.ImdbID,
Status: content.Status,
Budget: content.Budget,
Revenue: content.Revenue,
Runtime: content.Runtime,
}
err = saveContent(db, &c, onlyUpdate)
if err != nil {
slog.Error("cacheContentMovie: Failed to save content!", "error", err)
return Content{}, errors.New("failed to save content")
}
return c, nil
}
// Getting only region needed from api is not a feature yet
// https://trello.com/c/75tR4cpF/106-add-watch-provider-region-filtering
// When it is, this can be removed for that instead.
func transformProviders(c *interface{}, country string) {
slog.Debug("transformProviders called", "country", country)
if cmap, ok := (*c).(map[string]interface{}); ok {
if rmap, ok := cmap["results"].(map[string]interface{}); ok {
if val, ok := rmap[country]; ok {
slog.Debug("transformProviders: Found country.. overwriting whole object", "new_obj", val)
if rvmap, ok := val.(map[string]interface{}); ok {
rvmap["country"] = country
}
*c = val
} else {
slog.Warn("transformProviders: Couldn't find country..", "country", country)
}
} else {
slog.Warn("transformProviders: Couldn't find results property..")
}
} else {
slog.Error("transformProviders: Assertion failed")
}
}
func searchContent(query string) (TMDBSearchMultiResponse, error) {
resp := new(TMDBSearchMultiResponse)
err := tmdbRequest("/search/multi", map[string]string{"query": query, "page": "1"}, &resp)
if err != nil {
println("Failed to complete multi search request!", err.Error())
slog.Error("Failed to complete multi search request!", "error", err.Error())
return TMDBSearchMultiResponse{}, errors.New("failed to complete multi search request")
}
return *resp, nil
}
func movieDetails(id string) (TMDBMovieDetails, error) {
func movieDetails(db *gorm.DB, id string, country string, rParams map[string]string) (TMDBMovieDetails, error) {
resp := new(TMDBMovieDetails)
err := tmdbRequest("/movie/"+id, map[string]string{}, &resp)
err := tmdbRequest("/movie/"+id, rParams, &resp)
if err != nil {
println("Failed to complete movie details request!", err.Error())
slog.Error("Failed to complete movie details request!", "error", err.Error())
return TMDBMovieDetails{}, errors.New("failed to complete movie details request")
}
transformProviders(&resp.WatchProviders, country)
go cacheContentMovie(db, *resp, true)
return *resp, nil
}
@@ -45,19 +220,21 @@ func movieCredits(id string) (TMDBContentCredits, error) {
resp := new(TMDBContentCredits)
err := tmdbRequest("/movie/"+id+"/credits", map[string]string{}, &resp)
if err != nil {
println("Failed to complete movie cast request!", err.Error())
slog.Error("Failed to complete movie cast request!", "error", err.Error())
return TMDBContentCredits{}, errors.New("failed to complete movie cast request")
}
return *resp, nil
}
func tvDetails(id string) (TMDBShowDetails, error) {
func tvDetails(db *gorm.DB, id string, country string, rParams map[string]string) (TMDBShowDetails, error) {
resp := new(TMDBShowDetails)
err := tmdbRequest("/tv/"+id, map[string]string{}, &resp)
err := tmdbRequest("/tv/"+id, rParams, &resp)
if err != nil {
println("Failed to complete tv details request!", err.Error())
slog.Error("Failed to complete tv details request!", "error", err.Error())
return TMDBShowDetails{}, errors.New("failed to complete tv details request")
}
transformProviders(&resp.WatchProviders, country)
go cacheContentTv(db, *resp, true)
return *resp, nil
}
@@ -65,17 +242,27 @@ func tvCredits(id string) (TMDBContentCredits, error) {
resp := new(TMDBContentCredits)
err := tmdbRequest("/tv/"+id+"/credits", map[string]string{}, &resp)
if err != nil {
println("Failed to complete tv cast request!", err.Error())
slog.Error("Failed to complete tv cast request!", "error", err.Error())
return TMDBContentCredits{}, errors.New("failed to complete tv cast request")
}
return *resp, nil
}
func seasonDetails(tvId string, seasonNumber string) (TMDBSeasonDetails, error) {
resp := new(TMDBSeasonDetails)
err := tmdbRequest("/tv/"+tvId+"/season/"+seasonNumber, map[string]string{}, &resp)
if err != nil {
slog.Error("Failed to complete season details request!", "error", err.Error())
return TMDBSeasonDetails{}, errors.New("failed to complete season details request")
}
return *resp, nil
}
func personDetails(id string) (TMDBPersonDetails, error) {
resp := new(TMDBPersonDetails)
err := tmdbRequest("/person/"+id, map[string]string{}, &resp)
if err != nil {
println("Failed to complete person details request!", err.Error())
slog.Error("Failed to complete person details request!", "error", err.Error())
return TMDBPersonDetails{}, errors.New("failed to complete person details request")
}
return *resp, nil
@@ -85,8 +272,62 @@ func personCredits(id string) (TMDBPersonCombinedCredits, error) {
resp := new(TMDBPersonCombinedCredits)
err := tmdbRequest("/person/"+id+"/combined_credits", map[string]string{}, &resp)
if err != nil {
println("Failed to complete person details request!", err.Error())
slog.Error("Failed to complete person details request!", "error", err.Error())
return TMDBPersonCombinedCredits{}, errors.New("failed to complete person details request")
}
return *resp, nil
}
func discoverMovies() (TMDBDiscoverMovies, error) {
resp := new(TMDBDiscoverMovies)
err := tmdbRequest("/discover/movie", map[string]string{"page": "1"}, &resp)
if err != nil {
slog.Error("Failed to complete discover movies request!", "error", err.Error())
return TMDBDiscoverMovies{}, errors.New("failed to complete discover movies request")
}
return *resp, nil
}
func discoverTv() (TMDBDiscoverShows, error) {
resp := new(TMDBDiscoverShows)
err := tmdbRequest("/discover/tv", map[string]string{"page": "1"}, &resp)
if err != nil {
slog.Error("Failed to complete discover tv request!", "error", err.Error())
return TMDBDiscoverShows{}, errors.New("failed to complete discover tv request")
}
return *resp, nil
}
func allTrending() (TMDBTrendingAll, error) {
resp := new(TMDBTrendingAll)
err := tmdbRequest("/trending/all/day", map[string]string{}, &resp)
if err != nil {
slog.Error("Failed to complete all trending request!", "error", err.Error())
return TMDBTrendingAll{}, errors.New("failed to complete all trending request")
}
return *resp, nil
}
func upcomingMovies() (TMDBUpcomingMovies, error) {
resp := new(TMDBUpcomingMovies)
err := tmdbRequest("/movie/upcoming", map[string]string{"page": "1"}, &resp)
if err != nil {
slog.Error("Failed to complete upcoming movies request!", "error", err.Error())
return TMDBUpcomingMovies{}, errors.New("failed to complete upcoming movies request")
}
return *resp, nil
}
// Theres no upcoming endpoint for tv ;( - using discover with future dates
func upcomingTv() (TMDBUpcomingShows, error) {
resp := new(TMDBUpcomingShows)
dFmt := "2006-01-02"
mind := time.Now().Format(dFmt)
maxd := time.Now().AddDate(0, 0, 15).Format(dFmt)
err := tmdbRequest("/discover/tv", map[string]string{"page": "1", "first_air_date.gte": mind, "first_air_date.lte": maxd, "sort_by": "popularity.desc", "with_type": "2|3"}, &resp)
if err != nil {
slog.Error("Failed to complete upcoming tv request!", "error", err.Error())
return TMDBUpcomingShows{}, errors.New("failed to complete upcoming tv request")
}
return *resp, nil
}
+158
View File
@@ -0,0 +1,158 @@
package main
import (
"errors"
"log/slog"
"time"
"gorm.io/gorm"
)
// Database struct, only internal.
type Follow struct {
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"-"`
UserID uint `gorm:"primaryKey:usr_id_to_followed_id;not null;check:user_id != followed_user_id" json:"-"`
User User `json:"-"`
FollowedUserID uint `gorm:"primaryKey:usr_id_to_followed_id;not null" json:"-"`
FollowedUser User `json:"-"`
}
// For end users to see.
type FollowPublic struct {
CreatedAt time.Time `json:"createdAt"`
FollowedUser PublicUser `json:"followedUser"`
}
type FollowThoughts struct {
FollowedUser PublicUser `json:"followedUser"`
Thoughts string `json:"thoughts"`
Status WatchedStatus `json:"status"`
Rating int8 `json:"rating"`
}
func followUser(db *gorm.DB, currentUserId uint, toFollowUserId uint) (FollowPublic, error) {
f := Follow{UserID: currentUserId, FollowedUserID: toFollowUserId}
res := db.Model(&Follow{}).Create(&f)
if res.Error != nil {
slog.Error("followUser: Error on inserting follow.", "error", res.Error)
err := "failed to insert follow"
if res.Error == gorm.ErrDuplicatedKey {
err = "already followed"
}
return FollowPublic{}, errors.New(err)
}
// Now get the row with preloaded followed user
var nf Follow
res = db.Where("user_id = ? AND followed_user_id = ?", currentUserId, toFollowUserId).Preload("FollowedUser", "private = ?", 0).Take(&nf)
if res.Error != nil {
slog.Error("followUser: Couldn't fetch newly followed user.", "error", res.Error)
return FollowPublic{}, errors.New("followed, but failed to fetch followed user")
}
return FollowPublic{CreatedAt: nf.CreatedAt, FollowedUser: nf.FollowedUser.GetSafe()}, nil
}
func unfollowUser(db *gorm.DB, currentUserId uint, toFollowUserId uint) (bool, error) {
f := Follow{UserID: currentUserId, FollowedUserID: toFollowUserId}
res := db.Delete(&f)
if res.Error != nil {
slog.Error("unfollowUser: Error deleting follow.", "error", res.Error)
err := "failed to remove follow"
if res.Error == gorm.ErrRecordNotFound {
err = "not following"
}
return false, errors.New(err)
}
return true, nil
}
// Get current users follows
func getFollows(db *gorm.DB, userId uint) ([]FollowPublic, error) {
var follows []Follow
res := db.Where("user_id = ?", userId).Preload("FollowedUser", "private = ?", 0).Find(&follows)
if res.Error != nil {
slog.Error("getFollows: Error finding follows.", "error", res.Error)
return []FollowPublic{}, errors.New("failed to find follows")
}
fpub := []FollowPublic{}
for _, v := range follows {
// Skip followed users without an ID..
// this will be because they have made
// their account private after we followed them.
if v.FollowedUser.ID == 0 {
continue
}
fpub = append(fpub, FollowPublic{CreatedAt: v.CreatedAt, FollowedUser: v.FollowedUser.GetSafe()})
}
return fpub, nil
}
// Get followed profile thoughts, rating, etc on specific content.
func getFollowsThoughts(db *gorm.DB, userId uint, mediaType string, mediaId string) ([]FollowThoughts, error) {
var follows []Follow
res := db.Where("user_id = ?", userId).Preload("FollowedUser", "private = ? AND private_thoughts = ?", 0, 0).Find(&follows)
if res.Error != nil {
slog.Error("getFollows: Error finding follows.", "error", res.Error)
return []FollowThoughts{}, errors.New("failed to find follows")
}
slog.Info("getFollowsThoughts")
var followIds []uint
for _, v := range follows {
// Skip empty followedUsers.. they are private.
if v.FollowedUser.ID == 0 {
continue
}
followIds = append(followIds, v.FollowedUser.ID)
}
var contentOrGameId int
if mediaType == "game" {
// Get our content id from type and tmdbId
var content Game
res = db.Where("igdb_id = ?", mediaId).Select("id").Find(&content)
if res.Error != nil {
slog.Error("getFollows: Error finding content from db.", "error", res.Error)
return []FollowThoughts{}, errors.New("failed to find content")
}
contentOrGameId = content.ID
} else if mediaType == "movie" || mediaType == "tv" {
// Get our content id from type and tmdbId
var content Content
res = db.Where("type = ? AND tmdb_id = ?", mediaType, mediaId).Select("id").Find(&content)
if res.Error != nil {
slog.Error("getFollows: Error finding content from db.", "error", res.Error)
return []FollowThoughts{}, errors.New("failed to find content")
}
contentOrGameId = content.ID
} else {
slog.Error("getFollows: Unrecognized media type (movie, tv or game supported).", "media_type", mediaType)
return []FollowThoughts{}, errors.New("unrecognized media type")
}
// Get list of followeds watcheds for this content
var fw []Watched
if mediaType == "game" {
res = db.Where("game_id = ? AND user_id IN ?", contentOrGameId, followIds).Find(&fw)
} else {
res = db.Where("content_id = ? AND user_id IN ?", contentOrGameId, followIds).Find(&fw)
}
if res.Error != nil {
slog.Error("getFollows: Error finding followed watcheds from db.", "error", res.Error)
return []FollowThoughts{}, errors.New("failed to find followed watcheds")
}
// Create followThoughts array by combining follows and fw(atcheds)
ft := []FollowThoughts{}
for _, v := range fw {
var fu PublicUser
for _, f := range follows {
if f.FollowedUser.ID == v.UserID {
fu = f.FollowedUser.GetSafe()
break
}
}
// If we didn't find a related followedUser.. skip this watched entry
if fu.ID == 0 {
continue
}
ft = append(ft, FollowThoughts{FollowedUser: fu, Thoughts: v.Thoughts, Status: v.Status, Rating: v.Rating})
}
return ft, nil
}
+276
View File
@@ -0,0 +1,276 @@
package game
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"log/slog"
"net/http"
"net/url"
"time"
)
const (
igdbHost = "https://api.igdb.com/v4"
tokenGrantType = "client_credentials"
)
var tokenRefreshJobCancel context.CancelFunc
type IGDB struct {
ClientID *string `json:"clientId,omitempty"`
ClientSecret *string `json:"clientSecret,omitempty"`
AccessToken string `json:"accessToken,omitempty"`
AccessTokenExpires time.Time `json:"accessTokenExpires,omitempty"`
onTokenRefreshed *func()
}
func (i *IGDB) req(host string, ep string, p map[string]string, b string, resp interface{}) error {
// if using igdb host and we have no access token, error before running req
if host == igdbHost && (i.ClientID == nil || i.AccessToken == "") {
return errors.New("using igdbHost without a clientID or accessToken")
}
base, err := url.Parse(host)
if err != nil {
return errors.New("failed to parse api uri")
}
// Path params
base.Path += ep
// Query params
params := url.Values{}
for k, v := range p {
params.Add(k, v)
}
// Add params to url
base.RawQuery = params.Encode()
slog.Info("req", "base", base.String())
req, err := http.NewRequest("POST", base.String(), bytes.NewBuffer([]byte(b)))
if err != nil {
return err
}
// Add igdb auth headers
if host == igdbHost {
req.Header.Add("Client-ID", *i.ClientID)
req.Header.Add("Authorization", "Bearer "+i.AccessToken)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
body, err := io.ReadAll(res.Body)
res.Body.Close()
if err != nil {
return err
}
if !(res.StatusCode >= 200 && res.StatusCode <= 299) {
slog.Error("game non 2xx status code:", "status_code", res.StatusCode)
return errors.New(string(body))
}
err = json.Unmarshal([]byte(body), &resp)
if err != nil {
return err
}
return nil
}
func (i *IGDB) getNewAccessToken() (TwitchTokenResponse, error) {
var resp TwitchTokenResponse
err := i.req(
"https://id.twitch.tv/oauth2",
"/token",
map[string]string{"client_id": *i.ClientID, "client_secret": *i.ClientSecret, "grant_type": tokenGrantType},
"",
&resp,
)
if err != nil {
slog.Error("IGDB init token request failed", "error", err)
return TwitchTokenResponse{}, errors.New("token request failed, check client id and secret")
}
return resp, nil
}
func (i *IGDB) refreshToken(ctx context.Context) {
var exp <-chan time.Time
if i.AccessToken != "" && !i.AccessTokenExpires.IsZero() && i.AccessTokenExpires.Compare(time.Now()) == 1 {
// Stored token not expired.. set exp time to token exp time - 1h.
exp = time.After(i.AccessTokenExpires.Sub(time.Now().Add(60 * time.Second)))
} else {
// Token expired.. exp now..
exp = time.After(100 * time.Millisecond)
}
slog.Info("refreshToken running")
for {
select {
case <-exp:
slog.Info("IGDB refreshToken: Token expired (or is near expiry date)")
r, err := i.getNewAccessToken()
if err != nil {
slog.Error("IGDB refreshToken: Error refreshing token (retrying in 60s):", err)
exp = time.After(60 * time.Second)
} else {
slog.Info("IGDB refreshToken: Token successfully refreshed")
i.AccessToken = r.AccessToken
i.AccessTokenExpires = time.Now().Add(time.Duration(r.ExpiresIn) * time.Second)
exp = time.After(i.AccessTokenExpires.Sub(time.Now().Add(60 * time.Second)))
// Call token refresh callback if set
if i.onTokenRefreshed != nil {
(*i.onTokenRefreshed)()
}
}
case <-ctx.Done():
slog.Info("refreshToken cancelled")
return
}
}
}
func (i *IGDB) OnTokenRefreshed(tokenRefreshed func()) {
i.onTokenRefreshed = &tokenRefreshed
}
// Get token and stuff
func (i *IGDB) Init() error {
// Cancel existing refresh job if we have a cancel func for its context
if tokenRefreshJobCancel != nil {
slog.Debug("IGDB init: Refresh job running.. cancelling it.")
tokenRefreshJobCancel()
tokenRefreshJobCancel = nil
}
// Stop here if we have no client id or secret.
if i.ClientID == nil || i.ClientSecret == nil {
slog.Error("IGDB init client id and or secret not provided")
return errors.New("client id and or secret not provided")
}
slog.Debug("IGDB init running.")
ctx, cancel := context.WithCancel(context.Background())
tokenRefreshJobCancel = cancel
// Get and set first token if needed
go i.refreshToken(ctx)
return nil
}
func (i *IGDB) Search(q string) (GameSearchResponse, error) {
slog.Debug("IGDB Search called", "query", q)
var resp GameSearchResponse
err := i.req(
igdbHost,
"/games",
map[string]string{},
"fields name, cover.image_id, version_title, summary, first_release_date; search \""+q+"\";",
&resp,
)
if err != nil {
slog.Error("IGDB Search request failed!", "error", err)
return GameSearchResponse{}, errors.New("request failed")
}
return resp, nil
}
func (i *IGDB) GameDetails(id string) (GameDetailsResponse, error) {
slog.Debug("IGDB GameDetails called", "id", id)
var resp []GameDetailsResponse
err := i.req(
igdbHost,
"/games",
map[string]string{},
`fields
name,
cover.image_id,
version_title,
summary,
storyline,
first_release_date,
artworks.width,
artworks.height,
artworks.image_id,
category,
platforms.name,
game_modes.name,
genres.id,
genres.name,
involved_companies.developer,
involved_companies.publisher,
involved_companies.porting,
involved_companies.supporting,
involved_companies.company.name,
involved_companies.company.description,
involved_companies.company.slug,
involved_companies.company.websites.category,
involved_companies.company.websites.trusted,
involved_companies.company.websites.url,
rating,
rating_count,
status,
url,
websites.trusted,
websites.category,
websites.url,
videos.name,
videos.video_id,
similar_games.id,
similar_games.name,
similar_games.summary,
similar_games.cover.image_id,
similar_games.first_release_date;
where id = `+id+";",
&resp,
)
if err != nil {
slog.Error("IGDB GameDetails request failed!", "error", err)
return GameDetailsResponse{}, errors.New("request failed")
}
if len(resp) > 0 {
return resp[0], nil
}
return GameDetailsResponse{}, errors.New("no game details recieved")
}
// Basic game details for when we are using them only to update our cache.
// In these cases, it's a waste to ask for everything, when we don't need it.
func (i *IGDB) GameDetailsBasic(id string) (GameDetailsBasicResponse, error) {
slog.Debug("IGDB GameDetails called", "id", id)
var resp []GameDetailsBasicResponse
err := i.req(
igdbHost,
"/games",
map[string]string{},
`fields
name,
cover.image_id,
summary,
storyline,
first_release_date,
category,
platforms.name,
game_modes.name,
genres.name,
rating,
rating_count,
status;
where id = `+id+";",
&resp,
)
if err != nil {
slog.Error("IGDB GameDetails request failed!", "error", err)
return GameDetailsBasicResponse{}, errors.New("request failed")
}
if len(resp) > 0 {
return resp[0], nil
}
return GameDetailsBasicResponse{}, errors.New("no game details recieved")
}
+139
View File
@@ -0,0 +1,139 @@
package game
import (
"encoding/json"
"time"
)
type TwitchTokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
}
// So we can unmarshall the unix timestamps returned from igdb into time.Time.
type UnixTime struct {
time.Time
}
func (u *UnixTime) UnmarshalJSON(b []byte) error {
var timestamp int64
err := json.Unmarshal(b, &timestamp)
if err != nil {
return err
}
u.Time = time.Unix(timestamp, 0)
return nil
}
// Only the fields we request included in each struct
type GameSearchResponse []struct {
ID int `json:"id"`
Cover struct {
ID int `json:"id"`
ImageID string `json:"image_id"`
} `json:"cover"`
FirstReleaseDate UnixTime `json:"first_release_date"`
Name string `json:"name"`
Summary string `json:"summary,omitempty"`
VersionTitle string `json:"version_title,omitempty"`
}
type GameDetailsResponse struct {
ID int `json:"id"`
Artworks []struct {
Width int `json:"width"`
Height int `json:"height"`
ImageID string `json:"image_id"`
} `json:"artworks"`
Category int `json:"category"`
Cover struct {
ID int `json:"id"`
ImageID string `json:"image_id"`
} `json:"cover"`
FirstReleaseDate UnixTime `json:"first_release_date"`
GameModes []struct {
ID int `json:"id"`
Name string `json:"name"`
} `json:"game_modes"`
Genres []struct {
ID int `json:"id"`
Name string `json:"name"`
} `json:"genres"`
InvolvedCompanies []struct {
ID int `json:"id"`
Company struct {
ID int `json:"id"`
Description string `json:"description"`
Name string `json:"name"`
Slug string `json:"slug"`
Websites []struct {
ID int `json:"id"`
Category int `json:"category"`
Trusted bool `json:"trusted"`
URL string `json:"url"`
} `json:"websites"`
} `json:"company"`
Developer bool `json:"developer"`
Porting bool `json:"porting"`
Publisher bool `json:"publisher"`
Supporting bool `json:"supporting"`
} `json:"involved_companies"`
Name string `json:"name"`
Platforms []struct {
ID int `json:"id"`
Name string `json:"name"`
} `json:"platforms"`
Rating float64 `json:"rating"`
RatingCount int `json:"rating_count"`
Summary string `json:"summary"`
Storyline string `json:"storyline"`
Status int `json:"status"`
URL string `json:"url"`
Videos []struct {
ID int `json:"id"`
Name string `json:"name"`
VideoID string `json:"video_id"`
} `json:"videos"`
Websites []struct {
ID int `json:"id"`
Category int `json:"category"`
Trusted bool `json:"trusted"`
URL string `json:"url"`
} `json:"websites"`
SimilarGame []struct {
ID int `json:"id"`
Name string `json:"name"`
Summary string `json:"summary"`
FirstReleaseDate UnixTime `json:"first_release_date"`
Cover struct {
ID int `json:"id"`
ImageID string `json:"image_id"`
} `json:"cover"`
} `json:"similar_games"`
}
type GameDetailsBasicResponse struct {
ID int `json:"id"`
Category int `json:"category"`
Cover struct {
ImageID string `json:"image_id"`
} `json:"cover"`
FirstReleaseDate UnixTime `json:"first_release_date"`
GameModes []struct {
Name string `json:"name"`
} `json:"game_modes"`
Genres []struct {
Name string `json:"name"`
} `json:"genres"`
Name string `json:"name"`
Platforms []struct {
Name string `json:"name"`
} `json:"platforms"`
Rating float64 `json:"rating"`
RatingCount int `json:"rating_count"`
Summary string `json:"summary"`
Storyline string `json:"storyline"`
Status int `json:"status"`
}
+212
View File
@@ -0,0 +1,212 @@
package main
import (
"encoding/json"
"errors"
"log/slog"
"strconv"
"time"
"github.com/sbondCo/Watcharr/game"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// For storing cached games, so we can serve the basic local data for watched list to work
type Game struct {
ID int `json:"id" gorm:"primaryKey;autoIncrement"`
UpdatedAt time.Time `json:"updatedAt"`
IgdbID int `json:"igdbId" gorm:"uniqueIndex;not null"`
Name string `json:"name"`
CoverID string `json:"coverId"`
Summary string `json:"summary"`
Storyline string `json:"storyline"`
// First release date
ReleaseDate *time.Time `json:"releaseDate,omitempty"`
Rating float64 `json:"rating"`
RatingCount int `json:"ratingCount"`
Status int `json:"status"`
Category int `json:"category"`
// Arrays turned to strings that may be useful
GameModes string `json:"gameModes"`
Genres string `json:"genres"`
Platforms string `json:"platforms"`
// Id to poster image row (cached game cover)
PosterID *uint `json:"-"`
Poster *Image `json:"poster,omitempty"`
}
type PlayedAddRequest struct {
Status WatchedStatus `json:"status"`
Rating int8 `json:"rating" binding:"max=10"`
IgdbID int `json:"igdbId" binding:"required"`
}
// Cache(save) game to our table
func saveGame(db *gorm.DB, c *Game, onlyUpdate bool) error {
slog.Info("Saving game to db", "id", c.IgdbID, "name", c.Name)
if c.IgdbID == 0 || c.Name == "" {
slog.Error("saveGame: content missing id or name!", "id", c.IgdbID, "name", c.Name)
return errors.New("game missing id or title")
}
if c.CoverID != "" {
p, err := downloadAndInsertImage(db, "https://images.igdb.com/igdb/image/upload/t_cover_big/"+c.CoverID+".png", "games")
if err != nil {
slog.Error("saveGame: Failed to cache game cover.", "error", err)
} else {
slog.Debug("saveGame: Cached game cover", "p", p)
c.PosterID = &p.ID
}
}
var res *gorm.DB
if onlyUpdate {
// We only want to update an existing row, if it exists.
res = db.Model(&Game{}).Where("igdb_id = ?", c.IgdbID).Updates(c)
if res.Error != nil {
slog.Error("saveGame: Error updating game in database", "error", res.Error.Error())
return errors.New("failed to update cached game in database")
}
} else {
// On conflict, update existing row with details incase any were updated/missing.
res = db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "igdb_id"}},
DoUpdates: clause.AssignmentColumns([]string{
"name",
"cover_id",
"summary",
"storyline",
"release_date",
"rating",
"rating_count",
"status",
"game_modes",
"genres",
}),
}).Create(&c)
if res.Error != nil {
// Error if anything but unique contraint error
if res.Error != gorm.ErrDuplicatedKey {
slog.Error("saveGame: Error creating game in database", "error", res.Error.Error())
return errors.New("failed to cache game in database")
}
}
}
return nil
}
func cacheGame(db *gorm.DB, g game.GameDetailsBasicResponse, onlyUpdate bool) (Game, error) {
slog.Debug("cacheGame", "game_details", g)
var (
gameModes string
genres string
platforms string
)
if len(g.GameModes) > 0 {
for _, v := range g.GameModes {
gameModes += v.Name + "|"
}
}
if len(g.Genres) > 0 {
for _, v := range g.Genres {
genres += v.Name + "|"
}
}
if len(g.Platforms) > 0 {
for _, v := range g.Platforms {
platforms += v.Name + "|"
}
}
c := Game{
IgdbID: g.ID,
Name: g.Name,
CoverID: g.Cover.ImageID,
Summary: g.Summary,
Storyline: g.Storyline,
ReleaseDate: &g.FirstReleaseDate.Time,
Rating: (g.Rating),
RatingCount: g.RatingCount,
Status: g.Status,
Category: g.Category,
GameModes: gameModes,
Genres: genres,
Platforms: platforms,
}
err := saveGame(db, &c, onlyUpdate)
if err != nil {
slog.Error("cacheGame: Failed to save game!", "error", err)
return Game{}, errors.New("failed to save game")
}
return c, nil
}
// For adding/updating played games, we will reuse methods defined in watched.go where easily possible.
func addPlayed(db *gorm.DB, igdb *game.IGDB, userId uint, ar PlayedAddRequest, at ActivityType) (Watched, error) {
slog.Debug("Adding played item", "userId", userId, "igdbId", ar.IgdbID)
var game Game
db.Where("igdb_id = ?", ar.IgdbID).Find(&game)
// Create game if not found from our db
if game == (Game{}) {
slog.Debug("Game not in db, fetching...")
resp, err := igdb.GameDetailsBasic(strconv.Itoa(ar.IgdbID))
if err != nil {
slog.Error("addPlayed content tmdb api request failed", "error", err)
return Watched{}, errors.New("failed to find requested games")
}
game, err = cacheGame(db, resp, false)
if err != nil {
slog.Error("addPlayed failed to cache game", "igdb_id", ar.IgdbID, "err", err)
return Watched{}, errors.New("failed to cache content")
}
}
// Error if content has no id
if game.ID == 0 {
return Watched{}, errors.New("failed to find game by id")
}
// Create watched entry in db
if ar.Status == "" {
ar.Status = FINISHED
}
watched := Watched{Status: ar.Status, Rating: ar.Rating, UserID: userId, GameID: &game.ID}
res := db.Create(&watched)
if res.Error != nil {
if res.Error == gorm.ErrDuplicatedKey {
res = db.Model(&Watched{}).Unscoped().Preload("Activity").Where("user_id = ? AND content_id = ?", userId, watched.ContentID).Take(&watched)
if res.Error != nil {
return Watched{}, errors.New("content already on watched list. errored checking for soft deleted record")
}
if watched.DeletedAt.Time.IsZero() {
return Watched{}, errors.New("content already on watched list")
} else {
slog.Info("addPlayed: Watched list item for this content exists as soft deleted record.. attempting to restore")
res = db.Model(&Watched{}).Unscoped().Where("user_id = ? AND content_id = ?", userId, watched.ContentID).Updates(map[string]interface{}{"status": ar.Status, "rating": ar.Rating, "deleted_at": nil})
watched.Status = ar.Status
watched.Rating = ar.Rating
if res.Error != nil {
slog.Error("addPlayed: Failed to restore soft deleted watch list item", "error", res.Error)
return Watched{}, errors.New("content already on watched list. errored removing soft delete timestamp")
}
}
} else {
slog.Error("Error adding watched content to database", "error", res.Error.Error())
return Watched{}, errors.New("failed adding content to database")
}
}
slog.Debug("Added watched list item", "item", watched)
var activity Activity
activityJson, err := json.Marshal(map[string]interface{}{"status": ar.Status, "rating": ar.Rating})
if err != nil {
slog.Error("Failed to marshal json for data in ADD_WATCHED activity request, adding without data", "error", err.Error())
activity, _ = addActivity(db, userId, ActivityAddRequest{WatchedID: watched.ID, Type: at})
} else {
activity, _ = addActivity(db, userId, ActivityAddRequest{WatchedID: watched.ID, Type: at, Data: string(activityJson)})
}
watched.Activity = append(watched.Activity, activity)
watched.Game = &game
return watched, nil
}
+33 -45
View File
@@ -1,64 +1,52 @@
module github.com/sbondCo/Watcharr
go 1.20
go 1.21
require (
github.com/gin-contrib/cors v1.4.0
github.com/gin-gonic/gin v1.9.0
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/buckket/go-blurhash v1.1.0
github.com/gin-contrib/cache v1.2.0
github.com/gin-contrib/cors v1.7.0
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9
github.com/uptrace/bun v1.1.14
github.com/uptrace/bun/dialect/sqlitedialect v1.1.14
github.com/uptrace/bun/driver/sqliteshim v1.1.14
golang.org/x/crypto v0.9.0
gorm.io/driver/sqlite v1.5.1
gorm.io/gorm v1.25.1
golang.org/x/crypto v0.21.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/driver/sqlite v1.5.5
gorm.io/gorm v1.25.7
)
require (
github.com/bytedance/sonic v1.8.3 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect
github.com/bytedance/sonic v1.11.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.11.2 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/go-playground/validator/v10 v10.19.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gomodule/redigo v1.8.9 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.2 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/memcachier/mc/v3 v3.0.3 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.7 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.10 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.9.1 // indirect
google.golang.org/protobuf v1.28.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.6 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/sqlite v1.22.1 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.1.0 // indirect
)
+76 -190
View File
@@ -1,240 +1,126 @@
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/buckket/go-blurhash v1.1.0 h1:X5M6r0LIvwdvKiUtiNcRL2YlmOfMzYobI3VCKCZc9Do=
github.com/buckket/go-blurhash v1.1.0/go.mod h1:aT2iqo5W9vu9GpyoLErKfTHwgODsZp3bQfXjXJUxNb8=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.8.3 h1:pf6fGl5eqWYKkx1RcD4qpuX+BIUaduv/wTm5ekWJ80M=
github.com/bytedance/sonic v1.8.3/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.11.2 h1:ywfwo0a/3j9HR8wsYGWsIWl2mvRsI950HyoxiBERw5A=
github.com/bytedance/sonic v1.11.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/cache v1.2.0 h1:WA+AJR4kmHDTaLLShCHo/IeWVmmGRZ3Lsr3JQ46tFlE=
github.com/gin-contrib/cache v1.2.0/go.mod h1:2KkFL8PSnPF3Tt5E2Jpc3HWuBAUKqGZnClCFMm0tXQI=
github.com/gin-contrib/cors v1.7.0 h1:wZX2wuZ0o7rV2/1i7gb4Jn+gW7HBqaP91fizJkBUJOA=
github.com/gin-contrib/cors v1.7.0/go.mod h1:cI+h6iOAyxKRtUtC6iF/Si1KSFvGm/gK+kshxlCi8ro=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.0.0-rc.1 h1:tDQ1LjKga657layZ4JLsRdxgvupebc0xuPwRNuTfUgs=
github.com/golang-jwt/jwt/v5 v5.0.0-rc.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4=
github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ=
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/memcachier/mc/v3 v3.0.3 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv4=
github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko=
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.2.10 h1:eimT6Lsr+2lzmSZxPhLFoOWFmQqwk0fllJJ5hEbTXtQ=
github.com/ugorji/go/codec v1.2.10/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/uptrace/bun v1.1.12 h1:sOjDVHxNTuM6dNGaba0wUuz7KvDE1BmNu9Gqs2gJSXQ=
github.com/uptrace/bun v1.1.12/go.mod h1:NPG6JGULBeQ9IU6yHp7YGELRa5Agmd7ATZdz4tGZ6z0=
github.com/uptrace/bun v1.1.14 h1:S5vvNnjEynJ0CvnrBOD7MIRW7q/WbtvFXrdfy0lddAM=
github.com/uptrace/bun v1.1.14/go.mod h1:RHk6DrIisO62dv10pUOJCz5MphXThuOTpVNYEYv7NI8=
github.com/uptrace/bun/dialect/sqlitedialect v1.1.12 h1:Ud31nqZmebcQpl151nb108+vtcpxJ7kfXmbPYbALBiI=
github.com/uptrace/bun/dialect/sqlitedialect v1.1.12/go.mod h1:Pwg7s31BdF3PMBlWTnYkEn2I9ASsvatt1Ln/AERCTV4=
github.com/uptrace/bun/dialect/sqlitedialect v1.1.14 h1:SlwXLxr+N1kEo8Q0cheRlnIZLZlWniEB1OI+jkiLgWE=
github.com/uptrace/bun/dialect/sqlitedialect v1.1.14/go.mod h1:9RTEj1l4bB9a4l1Mnc9y4COTwWlFYe1dh6fyxq1rR7A=
github.com/uptrace/bun/driver/sqliteshim v1.1.12 h1:GMbSa7Pjjk4kjF8XURz5uMLe2PbN98e6t00sp0rx2Eo=
github.com/uptrace/bun/driver/sqliteshim v1.1.12/go.mod h1:u67g2ewzoMDCCAqjliHAM/BJjEXfoExXlFXhx3TnXRs=
github.com/uptrace/bun/driver/sqliteshim v1.1.14 h1:DFPUJ6KjDP2myjq15gtYYNngmAFMww1Y2UFZv4tbUw8=
github.com/uptrace/bun/driver/sqliteshim v1.1.14/go.mod h1:5BFN7V6Sm37Tn7UE4FWNm/F6V3iJPUzAJ7QyRwA5b1k=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/driver/sqlite v1.5.1 h1:hYyrLkAWE71bcarJDPdZNTLWtr8XrSjOWyjUYI6xdL4=
gorm.io/driver/sqlite v1.5.1/go.mod h1:7MZZ2Z8bqyfSQA1gYEV6MagQWj3cpUkJj9Z+d1HEMEQ=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
modernc.org/libc v1.22.6 h1:cbXU8R+A6aOjRuhsFh3nbDWXO/Hs4ClJRXYB11KmPDo=
modernc.org/libc v1.22.6/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE=
modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
modernc.org/sqlite v1.21.0 h1:4aP4MdUf15i3R3M2mx6Q90WHKz3nZLoz96zlB6tNdow=
modernc.org/sqlite v1.21.0/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
modernc.org/sqlite v1.22.1 h1:P2+Dhp5FR1RlVRkQ3dDfCiv3Ok8XPxqpe70IjYVA9oE=
modernc.org/sqlite v1.22.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+16
View File
@@ -0,0 +1,16 @@
package main
import (
"crypto/rand"
b64 "encoding/base64"
)
// Generate a random string
func generateString(len int) (string, error) {
key := make([]byte, len)
_, err := rand.Read(key)
if err != nil {
return "", err
}
return b64.StdEncoding.EncodeToString([]byte(key)), nil
}
+201
View File
@@ -0,0 +1,201 @@
package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"image"
_ "image/jpeg"
_ "image/png"
"io"
"log"
"log/slog"
"mime/multipart"
"net/http"
"os"
"path"
"path/filepath"
"time"
"github.com/buckket/go-blurhash"
"gorm.io/gorm"
)
// For user uploaded images
type Image struct {
ID uint `gorm:"primarykey" json:"-"`
CreatedAt time.Time `json:"createdAt"`
Hash string `gorm:"uniqueIndex;not null" json:"-"`
BlurHash string `json:"blurHash"`
// Path constructable from hash alone, but I can't decide
// if I should have this or not so I figure it's easier
// to remove it later than to add it later....... -_-
Path string `gorm:"not null" json:"path"`
}
// Insert an image into database
func insertImage(db *gorm.DB, hash string, path string, f io.Reader) (Image, error) {
bh, _ := getBlurHash(f)
img := Image{
Hash: hash,
Path: path,
BlurHash: bh,
}
r := db.Where(Image{Hash: hash}).FirstOrCreate(&img)
if r.Error != nil {
slog.Error("insertImage firstOrCreate failed!", "error", r.Error)
return Image{}, errors.New("failed to select or create image")
}
return img, nil
}
func getBlurHash(img io.Reader) (string, error) {
i, _, err := image.Decode(img)
if err != nil {
// Handle errors
slog.Error("getBlurHash decoding image failed", "error", err)
return "", errors.New("decoding image failed")
}
bh, _ := blurhash.Encode(6, 5, i)
if err != nil {
// Handle errors
slog.Error("getBlurHash generation failed", "error", err)
return "", errors.New("blur hash generation failed")
}
slog.Debug("getBlurHash", "hash", bh)
return bh, nil
}
func cleanupImages(db *gorm.DB) {
slog.Info("cleanupImages running")
var unusedImgs []Image
// Select images that are not referenced by at least one other row.
// Currently only used for user avatars, add new tables when used.
db.Raw(`SELECT *
FROM images
WHERE NOT EXISTS (
SELECT 1
FROM users
WHERE users.avatar_id = images.id
);`).Scan(&unusedImgs)
slog.Info("cleanupImages: scanned for unused images", "amount", len(unusedImgs))
if len(unusedImgs) > 0 {
for _, v := range unusedImgs {
slog.Debug("cleanupImages: removing an image", "id", v.ID, "path", v.Path)
err := db.Transaction(func(tx *gorm.DB) error {
// Try to delete image from db
if err := tx.Where("id = ?", v.ID).Delete(&Image{}).Error; err != nil {
return err
}
// hope its ok to do this sorta thing here :skull:
if err := os.Remove(path.Join(DataPath, v.Path)); err != nil {
return err
}
// commit transaction if no errors
return nil
})
if err != nil {
slog.Error("cleanupImages: failed to remove image - db row and file kept", "img", v, "error", err)
} else {
slog.Debug("cleanupImages: successfully removed unused image.", "id", v.ID)
}
}
}
}
func isValidImageType(f multipart.File) error {
// Read first 512 bytes, since that is all `DetectContentType` will evaluate on.
// Reading whole file is a waste.
buff := make([]byte, 512)
if _, err := f.Read(buff); err != nil {
slog.Error("isValidImageType: failed to read file into buffer", "error", err)
return errors.New("failed to verify if image is valid")
}
t := http.DetectContentType(buff)
slog.Debug("isValidImageType", "type", t)
if t != "image/png" && t != "image/jpeg" && t != "image/webp" && t != "image/gif" {
slog.Debug("isValidImageType: rejecting file as not valid (supported) image type")
return errors.New("invalid file type")
}
return nil
}
func downloadAndInsertImage(db *gorm.DB, url string, imgSubPath string) (Image, error) {
slog.Debug("Attempting to download image", "url", url)
// Get the data
resp, err := http.Get(url)
if err != nil {
return Image{}, err
}
defer resp.Body.Close()
// Check server response
if resp.StatusCode != http.StatusOK {
return Image{}, fmt.Errorf("bad status: %s", resp.Status)
}
// Read body into byte array, then create new reader
// So we have the ability to seek.
b, err := io.ReadAll(resp.Body)
if err != nil {
slog.Error("downloadAndInsertImage failed to read response into byte array", "error", err)
return Image{}, err
}
br := bytes.NewReader(b)
h := sha256.New()
if _, err := io.Copy(h, br); err != nil {
log.Fatal(err) // TODO nu le fatal
}
hs := hex.EncodeToString(h.Sum(nil))
// Seek back for file
_, err = br.Seek(0, 0)
if err != nil {
slog.Error("downloadAndInsertImage seeking back to start of br failed", "error", err)
return Image{}, err
}
outp := path.Join("img/", imgSubPath, hs[0:1], hs+filepath.Ext(resp.Request.URL.Path))
dataOutP := path.Join(DataPath, outp)
// Create the file
out, err := os.Create(dataOutP)
if err != nil {
if os.IsNotExist(err) {
err = os.MkdirAll(path.Dir(dataOutP), 0764)
if err != nil {
return Image{}, err
}
// If dirs made, try making file again
out, err = os.Create(dataOutP)
if err != nil {
return Image{}, err
}
} else {
return Image{}, err
}
}
defer out.Close()
_, err = io.Copy(out, br)
if err != nil {
return Image{}, err
}
// Seek back for insertImage
_, err = br.Seek(0, 0)
if err != nil {
slog.Error("downloadAndInsertImage seeking back to start of br failed", "error", err)
return Image{}, err
}
img, err := insertImage(db, hs, outp, br)
if err != nil {
return Image{}, err
}
return img, nil
}
+174
View File
@@ -0,0 +1,174 @@
package main
import (
"encoding/json"
"errors"
"log/slog"
"strconv"
"strings"
"time"
"gorm.io/gorm"
)
type ImportResponseType string
var (
// Successful import
IMPORT_SUCCESS ImportResponseType = "IMPORT_SUCCESS"
// Import failed for reasons user cant fix
IMPORT_FAILED ImportResponseType = "IMPORT_FAILED"
// Import query returned multiple results, user must decide
IMPORT_MULTI ImportResponseType = "IMPORT_MULTI"
// Import query returned zero results, user must provide more info
IMPORT_NOTFOUND ImportResponseType = "IMPORT_NOTFOUND"
// Item already exists so couldn't import (unique constraint hit when adding)
IMPORT_EXISTS ImportResponseType = "IMPORT_EXISTS"
)
type ImportRequest struct {
Name string `json:"name"`
TmdbID int `json:"tmdbId"`
Type ContentType `json:"type"`
Rating int8 `json:"rating"`
RatingCustomDate *time.Time `json:"ratingCustomDate"`
Status WatchedStatus `json:"status"`
Thoughts string `json:"thoughts"`
DatesWatched []time.Time `json:"datesWatched"`
}
type ImportResponse struct {
Type ImportResponseType `json:"type"`
Results []TMDBSearchMultiResults `json:"results"`
Match TMDBSearchMultiResults `json:"match"`
// On success this will be filled with the new watched entry
WatchedEntry Watched `json:"watchedEntry"`
}
func importContent(db *gorm.DB, userId uint, ar ImportRequest) (ImportResponse, error) {
// If tmdbId and type passed in request body
// we dont need to use a search tmdb request.
// Retrieve the details directly.
if ar.TmdbID != 0 && (ar.Type == MOVIE || ar.Type == SHOW) {
tid := strconv.Itoa(ar.TmdbID)
if ar.Type == MOVIE {
cr, err := movieDetails(db, tid, "", map[string]string{})
if err != nil {
return ImportResponse{}, errors.New("movie details request failed")
}
slog.Debug("import: by tmdbid of movie", "cr", cr)
return successfulImport(db, userId, cr.ID, MOVIE, ar)
} else if ar.Type == SHOW {
cr, err := tvDetails(db, tid, "", map[string]string{})
if err != nil {
return ImportResponse{}, errors.New("tv details request failed")
}
slog.Debug("import: by tmdbid of tv", "cr", cr)
return successfulImport(db, userId, cr.ID, SHOW, ar)
}
}
sr, err := searchContent(ar.Name)
if err != nil {
slog.Error("import: content search failed", "error", err)
return ImportResponse{}, errors.New("Content search failed")
}
pMatches := []TMDBSearchMultiResults{}
for _, r := range sr.Results {
if r.MediaType != "person" {
pMatches = append(pMatches, r)
}
}
resLen := len(pMatches)
slog.Debug("import: potential matches", "num_found", resLen)
if resLen <= 0 {
slog.Debug("import: returning IMPORT_NOTFOUND")
return ImportResponse{Type: IMPORT_NOTFOUND}, nil
} else if resLen > 1 {
slog.Debug("import: multiple results found")
// If there are multiple responses, but only one item
// from the results is a 100% match for the imported
// items name, then consider successful match with that.
var perfectMatch TMDBSearchMultiResults
for _, r := range pMatches {
itemName := r.Name
if itemName == "" {
itemName = r.Title
}
if strings.EqualFold(itemName, ar.Name) {
slog.Debug("import: multiple results processing: found a perfectMatch", "match", r)
if perfectMatch.ID != 0 {
// If perfect match has been set before..
// quit looking and just show all results.
slog.Debug("import: multiple results processing: Second perfectMatch found.. returning all results")
return ImportResponse{Type: IMPORT_MULTI, Results: pMatches}, nil
}
perfectMatch = r
}
}
// If one perfect match found, import it
if perfectMatch.ID != 0 {
slog.Debug("import: importing from perfect match")
return successfulImport(db, userId, perfectMatch.ID, ContentType(perfectMatch.MediaType), ar)
}
return ImportResponse{Type: IMPORT_MULTI, Results: pMatches}, nil
} else {
slog.Debug("import: success.. only found one result")
return successfulImport(db, userId, pMatches[0].ID, ContentType(pMatches[0].MediaType), ar)
}
}
func successfulImport(db *gorm.DB, userId uint, contentId int, contentType ContentType, ar ImportRequest) (ImportResponse, error) {
status := FINISHED
if ar.Status != "" {
status = ar.Status
}
// Get the latest date from DatesWatched if we have any.
var wDate time.Time
if len(ar.DatesWatched) > 0 {
for _, dw := range ar.DatesWatched {
if dw.After(wDate) {
wDate = dw
}
}
}
w, err := addWatched(db, userId, WatchedAddRequest{
Status: status,
ContentID: contentId,
ContentType: contentType,
Rating: ar.Rating,
Thoughts: ar.Thoughts,
WatchedDate: wDate,
}, IMPORTED_WATCHED)
if err != nil {
if err.Error() == "content already on watched list" {
slog.Error("successfulImport: unique constraint hit.. show must already be on watch list", "error", err)
return ImportResponse{Type: IMPORT_EXISTS}, nil
}
slog.Error("successfulImport: Failed to add content as watched", "error", err)
return ImportResponse{Type: IMPORT_FAILED}, nil
}
// Add activity of the original time the show was added to the users watchlist on whichever platform they are coming from.
if ar.RatingCustomDate != nil {
var addedActivity Activity
if len(w.Activity) > 0 {
activityJson, _ := json.Marshal(map[string]interface{}{"rating": ar.Rating, "linkedActivity": w.Activity[0].ID})
addedActivity, _ = addActivity(db, userId, ActivityAddRequest{WatchedID: w.ID, Type: IMPORTED_RATING, Data: string(activityJson), CustomDate: ar.RatingCustomDate})
} else {
addedActivity, _ = addActivity(db, userId, ActivityAddRequest{WatchedID: w.ID, Type: IMPORTED_RATING, Data: strconv.Itoa(int(ar.Rating)), CustomDate: ar.RatingCustomDate})
}
w.Activity = append(w.Activity, addedActivity)
}
// Add all dates watched as activity, if any
if len(ar.DatesWatched) > 0 {
for _, v := range ar.DatesWatched {
customDate := v
addedActivity, err := addActivity(db, userId, ActivityAddRequest{WatchedID: w.ID, Type: IMPORTED_ADDED_WATCHED, CustomDate: &customDate})
if err == nil {
w.Activity = append(w.Activity, addedActivity)
} else {
slog.Error("successfulImport: Failed to add dateswatched activity.", "date", v, "error", err)
}
}
}
return ImportResponse{Type: IMPORT_SUCCESS, WatchedEntry: w}, nil
}
+191
View File
@@ -0,0 +1,191 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"io"
"log/slog"
"net/http"
"net/url"
"time"
"github.com/gin-gonic/gin"
)
type JellyfinItemSearchResponse struct {
Items []JellyfinItems `json:"Items"`
}
type JellyfinItems struct {
Name string `json:"Name"`
Type string `json:"Type"`
ServerID string `json:"ServerId"`
Id string `json:"Id"`
ProviderIds struct {
Tmdb string `json:"Tmdb"`
} `json:"ProviderIds"`
UserData struct {
Rating float64 `json:"Rating"`
PlayedPercentage float64 `json:"PlayedPercentage"`
UnplayedItemCount int64 `json:"UnplayedItemCount"`
PlaybackPositionTicks int64 `json:"PlaybackPositionTicks"`
PlayCount int64 `json:"PlayCount"`
IsFavorite bool `json:"IsFavorite"`
Likes bool `json:"Likes"`
LastPlayedDate time.Time `json:"LastPlayedDate"`
Played bool `json:"Played"`
Key string `json:"Key"`
ItemId string `json:"ItemId"`
} `json:"UserData"`
RecursiveItemCount int64 `json:"RecursiveItemCount"`
}
type JFContentFindResponse struct {
HasContent bool `json:"hasContent"`
Url string `json:"url"`
}
// Jellyfin access middleware, ensures user is a jellyfin user.
// To be ran after AuthRequired middleware with extra data.
func JellyfinAccessRequired() gin.HandlerFunc {
return func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
slog.Debug("JellyfinAccessRequired middleware hit", "user_id", userId)
userType := c.MustGet("userType").(UserType)
userThirdPartyId := c.MustGet("userThirdPartyId").(string)
userThirdPartyAuth := c.MustGet("userThirdPartyAuth").(string)
if Config.JELLYFIN_HOST == "" {
slog.Error("JellyfinAccessRequired: Request made to login via Jellyfin, but JELLYFIN_HOST has not been configured.")
c.AbortWithStatus(401)
return
}
if userType != JELLYFIN_USER || userThirdPartyId == "" {
slog.Error("JellyfinAccessRequired: User is not a jellyfin user..", "user_type", userType, "user_third_party_id", userThirdPartyId)
c.AbortWithStatus(401)
return
}
if userThirdPartyAuth == "" {
slog.Error("JellyfinAccessRequired: User has no thirdPartyAuth token..")
c.AbortWithStatus(401)
return
}
}
}
func jellyfinAPIRequest(method string, ep string, p map[string]string, username string, userToken string, resp interface{}) error {
if Config.JELLYFIN_HOST == "" {
slog.Error("jellyfinAPIRequest: JELLYFIN_HOST not configured.")
return errors.New("jellyfin not enabled")
}
slog.Debug("jellyfinAPIRequest", "endpoint", ep, "params", p)
base, err := url.Parse(Config.JELLYFIN_HOST)
if err != nil {
return errors.New("failed to parse api uri")
}
// Path params
base.Path += ep
// Query params
params := url.Values{}
for k, v := range p {
params.Add(k, v)
}
// Add params to url
base.RawQuery = params.Encode()
// Run get request
client := &http.Client{}
req, err := http.NewRequest(method, base.String(), bytes.NewBuffer([]byte{}))
if err != nil {
slog.Error("Creating request to jellyfin failed", "error", err)
return errors.New("request failed")
}
req.Header.Add("Content-Type", "application/json")
authHeader := "MediaBrowser Client=\"Watcharr\", Device=\"HTTP\", DeviceId=\"WatcharrFor" + username + "\", Version=\"10.8.0\""
if userToken != "" {
authHeader += ", Token=\"" + userToken + "\""
}
req.Header.Add("X-Emby-Authorization", authHeader)
res, err := client.Do(req)
if err != nil {
slog.Error("making request to jellyfin for auth failed", "error", err)
return errors.New("request failed")
}
body, err := io.ReadAll(res.Body)
res.Body.Close()
if err != nil {
slog.Error("Error reading jellyfin auth response", "error", err.Error())
return err
}
if res.StatusCode != 200 {
slog.Error("Jellyfin auth non 200 status code", "status_code", res.StatusCode, "error", string(body))
return errors.New("incorrect details")
}
// Unmarshal response
err = json.Unmarshal([]byte(body), &resp)
if err != nil {
return err
}
return nil
}
func jellyfinContentFind(
userId uint,
userType UserType,
username string,
userThirdPartyId string,
userThirdPartyAuth string,
contentType string,
contentName string,
contentTmdbId string,
) (JFContentFindResponse, error) {
if contentType == "" || contentName == "" {
slog.Error("Bad request", "content_type", contentType, "content_name", contentName)
return JFContentFindResponse{}, errors.New("content type or name not provided")
}
if contentType == "tv" {
contentType = "series"
}
resp := new(JellyfinItemSearchResponse)
err := jellyfinAPIRequest(
"GET",
"/Users/"+userThirdPartyId+"/Items",
map[string]string{
"searchTerm": contentName,
"IncludePeople": "false",
"IncludeMedia": "true",
"IncludeGenres": "false",
"IncludeStudios": "false",
"IncludeArtists": "false",
"IncludeItemTypes": contentType,
"Limit": "5",
"Fields": "ProviderIds",
"Recursive": "true",
"EnableTotalRecordCount": "false",
"ImageTypeLimit": "0",
},
username,
userThirdPartyAuth,
&resp,
)
if err != nil {
slog.Error("jellyfinContentFind: Jellyfin API request failed", "error", err)
return JFContentFindResponse{}, errors.New("failed to get jellyfin response")
}
// Find true match from jf search results
ret := new(JFContentFindResponse)
ret.HasContent = false
ret.Url = ""
for _, i := range resp.Items {
if i.ProviderIds.Tmdb == contentTmdbId {
ret.HasContent = true
ret.Url = Config.JELLYFIN_HOST + "/web/index.html#!/details?id=" + i.Id + "&serverId=" + i.ServerID
}
}
return *ret, nil
}
+311
View File
@@ -0,0 +1,311 @@
package main
import (
"errors"
"log/slog"
"strconv"
"gorm.io/gorm"
)
type JellyfinSeriesSeasonsResponse struct {
Items []JellyfinSeriesSeasonItem `json:"Items"`
}
type JellyfinSeriesSeasonItem struct {
JellyfinItems
// aka the season number
IndexNumber int `json:"IndexNumber"`
}
type JellyfinSeriesEpisodesResponse struct {
Items []JellyfinSeriesEpisodeItem `json:"Items"`
}
type JellyfinSeriesEpisodeItem struct {
JellyfinItems
// the episode number
IndexNumber int `json:"IndexNumber"`
// the episodes season number
ParentIndexNumber int `json:"ParentIndexNumber"`
}
type JellyfinSyncResponse struct {
JobId string `json:"jobId"`
}
// Perform the jellyfin sync.
// Gets each type of media separately from jellyfin and attempts to import them.
// Errors are added silently to the job.
func startJellyfinSync(
db *gorm.DB,
jobId string,
userId uint,
username string,
userThirdPartyId string,
userThirdPartyAuth string,
) {
// Get played movies
updateJobCurrentTask(jobId, userId, "syncing movies")
playedMovies := new(JellyfinItemSearchResponse)
err := jellyfinAPIRequest(
"GET",
"/Users/"+userThirdPartyId+"/Items",
map[string]string{
"Filters": "IsPlayed",
"IncludeItemTypes": "Movie",
"Fields": "ProviderIds",
"Recursive": "true",
},
username,
userThirdPartyAuth,
&playedMovies,
)
if err != nil {
slog.Error("jellyfinSyncWatched: Jellyfin API request failed", "error", err)
addJobError(jobId, userId, "failed to get jellyfin response for movies")
} else {
if len(playedMovies.Items) <= 0 {
slog.Info("jellyfinSyncWatched: User has no played movies.", "user_id", userId)
} else {
for _, v := range playedMovies.Items {
slog.Info("jellyfinSyncWatched: Importing played movie.", "movie_name", v.Name, "user_id", userId)
slog.Debug("jellyfinSyncWatched: Importing played movie.", "full_item", v, "user_id", userId)
// 1. Ensure we have a tmdbId
if v.ProviderIds.Tmdb == "" {
slog.Error("jellyfinSyncWatched: Movie to import does not have a tmdb id.", "movie_name", v.Name, "movie_ids", v.ProviderIds, "user_id", userId)
addJobError(jobId, userId, "movie could not be imported (no tmdbId present): "+v.Name)
continue
}
tmdbId, err := strconv.Atoi(v.ProviderIds.Tmdb)
if err != nil {
slog.Error("jellyfinSyncWatched: Movie to import does not have a parseable (to int) tmdb id.", "movie_name", v.Name, "movie_ids", v.ProviderIds, "user_id", userId)
addJobError(jobId, userId, "movie could not be imported (tmdbId was not parseable): "+v.Name)
continue
}
updateJobCurrentTask(jobId, userId, "syncing "+v.Name)
// 2. Imported watched movie
w, err := addWatched(db, userId, WatchedAddRequest{
Status: FINISHED,
ContentID: tmdbId,
ContentType: MOVIE,
WatchedDate: v.UserData.LastPlayedDate,
}, IMPORTED_WATCHED_JF)
if err != nil {
if err.Error() == "content already on watched list" {
slog.Error("jellyfinSyncWatched: Unique constraint hit.. content must already be on watch list.", "movie_name", v.Name, "movie_ids", v.ProviderIds, "user_id", userId)
} else {
slog.Error("jellyfinSyncWatched: Movie failed to import.", "movie_name", v.Name, "movie_ids", v.ProviderIds, "user_id", userId)
addJobError(jobId, userId, "movie could not be imported (failed when adding to watched list): "+v.Name)
}
} else {
// 3. Add IMPORTED_ADDED_WATCHED_JF activity
if !v.UserData.LastPlayedDate.IsZero() {
_, err := addActivity(db, userId, ActivityAddRequest{WatchedID: w.ID, Type: IMPORTED_ADDED_WATCHED_JF, CustomDate: &v.UserData.LastPlayedDate})
if err != nil {
slog.Error("jellyfinSyncWatched: Failed to add dateswatched activity.", "movie_name", v.Name,
"movie_ids", v.ProviderIds, "user_id", userId, "date", v.UserData.LastPlayedDate, "error", err)
}
}
}
}
}
}
// Get played series
// Can't rely on IsPlayed filter, since we want to get partially played series too.
updateJobCurrentTask(jobId, userId, "syncing series")
allSeries := new(JellyfinItemSearchResponse)
err = jellyfinAPIRequest(
"GET",
"/Users/"+userThirdPartyId+"/Items",
map[string]string{
"IncludeItemTypes": "Series",
"Fields": "ProviderIds,RecursiveItemCount",
"Recursive": "true",
"IsPlaceHolder": "false",
},
username,
userThirdPartyAuth,
&allSeries,
)
if err != nil {
slog.Error("jellyfinSyncWatched: Jellyfin API request failed", "error", err)
addJobError(jobId, userId, "failed to get jellyfin response for series")
} else {
if len(allSeries.Items) <= 0 {
slog.Info("jellyfinSyncWatched: No series found.", "user_id", userId)
} else {
// Import series
for _, v := range allSeries.Items {
slog.Info("jellyfinSyncWatched: Processing series.", "series_name", v.Name, "user_id", userId)
slog.Debug("jellyfinSyncWatched: Processing series.", "full_item", v, "user_id", userId)
// 1. Make sure show is watched or at least partially watched
if !v.UserData.Played && v.UserData.PlayedPercentage <= 0 && v.RecursiveItemCount == v.UserData.UnplayedItemCount {
slog.Debug("jellyfinSyncWatched: Skipping unwatched series:", "series_name", v.Name, "user_id", userId)
continue
}
// 1.1. Ensure we have a tmdbId
if v.ProviderIds.Tmdb == "" {
slog.Error("jellyfinSyncWatched: Series to import does not have a tmdb id.", "series_name", v.Name, "series_ids", v.ProviderIds, "user_id", userId)
addJobError(jobId, userId, "series could not be imported (no tmdbId present): "+v.Name)
continue
}
tmdbId, err := strconv.Atoi(v.ProviderIds.Tmdb)
if err != nil {
slog.Error("jellyfinSyncWatched: Series to import does not have a parseable (to int) tmdb id.", "series_name", v.Name, "series_ids", v.ProviderIds, "user_id", userId)
addJobError(jobId, userId, "series could not be imported (tmdbId was not parseable): "+v.Name)
continue
}
updateJobCurrentTask(jobId, userId, "syncing serie "+v.Name)
// 2. Imported watched series
w, err := addWatched(db, userId, WatchedAddRequest{
Status: FINISHED,
ContentID: tmdbId,
ContentType: SHOW,
WatchedDate: v.UserData.LastPlayedDate,
}, IMPORTED_WATCHED_JF)
if err != nil {
if err.Error() == "content already on watched list" {
slog.Info("jellyfinSyncWatched: Unique constraint hit.. content must already be on watch list.",
"series_name", v.Name, "series_ids", v.ProviderIds, "user_id", userId, "watched_id", w.ID)
} else {
slog.Error("jellyfinSyncWatched: Series failed to import.", "series_name", v.Name, "series_ids", v.ProviderIds, "user_id", userId)
addJobError(jobId, userId, "series could not be imported (failed when adding to watched list): "+v.Name)
}
} else {
// 3. Add IMPORTED_ADDED_WATCHED activity (only if no err above, show also must not have already been on our list)
if !v.UserData.LastPlayedDate.IsZero() {
_, err := addActivity(db, userId, ActivityAddRequest{WatchedID: w.ID, Type: IMPORTED_ADDED_WATCHED_JF, CustomDate: &v.UserData.LastPlayedDate})
if err != nil {
slog.Error("jellyfinSyncWatched: Failed to add dateswatched activity.", "series_name", v.Name,
"series_ids", v.ProviderIds, "user_id", userId, "date", v.UserData.LastPlayedDate, "error", err)
}
}
}
// 4. Import watched seasons for this serie
// Get all show seasons (filtering isPlayed doesn't seem to be a thing, so we will have to do that ourselves)
seriesSeasons := new(JellyfinSeriesSeasonsResponse)
err = jellyfinAPIRequest(
"GET",
"/Shows/"+v.Id+"/Seasons",
map[string]string{
"UserId": userThirdPartyId,
"Fields": "ProviderIds",
"IsPlaceHolder": "false",
},
username,
userThirdPartyAuth,
&seriesSeasons,
)
if err != nil {
slog.Error("jellyfinSyncWatched: Failed to fetch series seasons.", "series_name", v.Name, "series_ids", v.ProviderIds, "user_id", userId, "error", err)
addJobError(jobId, userId, "series seasons could not be imported (request failed): "+v.Name)
} else if len(seriesSeasons.Items) <= 0 {
slog.Info("jellyfinSyncWatched: Series has no seasons.", "series_name", v.Name, "series_ids", v.ProviderIds, "user_id", userId)
} else {
for _, vs := range seriesSeasons.Items {
slog.Debug("jellyfinSyncWatched: Processing a season.", "full_item", v, "user_id", userId)
if !vs.UserData.Played {
slog.Debug("jellyfinSyncWatched: Skipping import of unplayed season.", "series_name", v.Name, "season_num", vs.IndexNumber, "user_id", userId)
continue
}
updateJobCurrentTask(jobId, userId, "syncing "+v.Name+" season "+strconv.Itoa(vs.IndexNumber))
_, err = addWatchedSeason(db, userId, WatchedSeasonAddRequest{
WatchedID: w.ID,
SeasonNumber: vs.IndexNumber,
Status: FINISHED,
addActivity: SEASON_ADDED_JF,
addActivityDate: vs.UserData.LastPlayedDate,
})
if err != nil {
slog.Error("jellyfinSyncWatched: Failed to fetch series seasons.", "series_name", v.Name, "series_ids", v.ProviderIds, "user_id", userId, "error", err)
addJobError(jobId, userId, "series season could not be imported (addWatchedSeason request failed): "+v.Name+" season "+strconv.Itoa(vs.IndexNumber))
}
}
}
// 5. Import watched episodes for this serie
// Gets all show episodes (filtering isPlayed doesn't seem to be a thing, so we will have to do that ourselves)
seriesEpisodes := new(JellyfinSeriesEpisodesResponse)
err = jellyfinAPIRequest(
"GET",
"/Shows/"+v.Id+"/Episodes",
map[string]string{
"UserId": userThirdPartyId,
"Fields": "ProviderIds",
"IsPlaceHolder": "false",
},
username,
userThirdPartyAuth,
&seriesEpisodes,
)
if err != nil {
slog.Error("jellyfinSyncWatched: Failed to fetch series episodes.", "series_name", v.Name, "series_ids", v.ProviderIds, "user_id", userId, "error", err)
addJobError(jobId, userId, "series episodes could not be imported (request failed): "+v.Name)
} else if len(seriesEpisodes.Items) <= 0 {
slog.Info("jellyfinSyncWatched: Series has no episodes.", "series_name", v.Name, "series_ids", v.ProviderIds, "user_id", userId)
} else {
for _, vs := range seriesEpisodes.Items {
slog.Debug("jellyfinSyncWatched: Processing an episode.", "full_item", v, "user_id", userId)
if !vs.UserData.Played {
slog.Debug("jellyfinSyncWatched: Skipping import of unplayed episode.", "series_name", v.Name, "season_num", vs.ParentIndexNumber, "episode_num", vs.IndexNumber, "user_id", userId)
continue
}
updateJobCurrentTask(jobId, userId, "syncing "+v.Name+" season "+strconv.Itoa(vs.ParentIndexNumber)+" episode "+strconv.Itoa(vs.IndexNumber))
_, err = addWatchedEpisodes(db, userId, WatchedEpisodeAddRequest{
WatchedID: w.ID,
SeasonNumber: vs.ParentIndexNumber,
EpisodeNumber: vs.IndexNumber,
Status: FINISHED,
addActivity: EPISODE_ADDED_JF,
addActivityDate: vs.UserData.LastPlayedDate,
})
if err != nil {
slog.Error("jellyfinSyncWatched: Failed to import series episode.", "series_name", v.Name, "season_num", vs.ParentIndexNumber, "episode_num", vs.IndexNumber, "user_id", userId, "error", err)
addJobError(jobId, userId, "series episode could not be imported (addWatchedEpisode request failed): "+v.Name+" "+vs.Name)
}
}
}
}
}
}
updateJobStatus(jobId, userId, JOB_DONE)
}
func jellyfinSyncWatched(
db *gorm.DB,
userId uint,
userType UserType,
username string,
userThirdPartyId string,
userThirdPartyAuth string,
) (JellyfinSyncResponse, error) {
jobId, err := addJob("jf_sync", userId)
if err != nil {
slog.Error("jellyfinSyncWatched: Failed to create a job", "error", err)
return JellyfinSyncResponse{}, errors.New("failed to create job")
}
updateJobStatus(jobId, userId, JOB_RUNNING)
go startJellyfinSync(
db,
jobId,
userId,
username,
userThirdPartyId,
userThirdPartyAuth,
)
return JellyfinSyncResponse{JobId: jobId}, nil
}
+125
View File
@@ -0,0 +1,125 @@
// We will use jobs only for storing/retrieving active job statuses for the client.
// Running the job will be done wherever needed, but isn't handled here.
// When starting a job elsewhere, we should first add a job here as active to get an `id`,
// this id should be used to update the active job so the client can request job status updates.
package main
import (
"errors"
"log/slog"
"time"
)
type JobStatus string
var (
JOB_CREATED JobStatus = "CREATED"
JOB_RUNNING JobStatus = "RUNNING"
JOB_DONE JobStatus = "DONE"
JOB_CANCELLED JobStatus = "CANCELLED"
)
type Job struct {
// We can give the job a name simply for showing on the client.
Name string `json:"name"`
// The current status of the job.
Status JobStatus `json:"status"`
// The current task we are performing inside the job.
// Just so we can portray progress on the client by displaying the current task.
CurrentTask string `json:"currentTask,omitempty"`
// Errors that occurred in the task
Errors []string `json:"errors"`
// Stored for access control.
UserId uint `json:"-"`
}
var activeJobs = make(map[string]*Job)
// Add a job to our activeJobs map.
// Returns id of job on success, or error if failed to add.
func addJob(name string, userId uint) (string, error) {
idk, err := generateString(8)
if err != nil {
return "", err
}
_, ok := activeJobs[idk]
if ok {
// Lets just hope this doesn't happen, may the odds be with us.
return "", errors.New("job already exists with id generated, please try again")
}
activeJobs[idk] = &Job{
Name: name,
Status: JOB_CREATED,
UserId: userId,
}
return idk, nil
}
func rmJob(id string, userId uint) {
slog.Debug("rmJob: Removing a job.", "id", id)
v, ok := activeJobs[id]
if ok && v.UserId == userId {
delete(activeJobs, id)
slog.Debug("rmJob: Removed a job.", "id", id)
return
}
slog.Debug("rmJob: Job to remove does not exist (or not owned by this user).", "id", id, "user_id", userId)
}
// Get a job.
// Returns job if found, otherwise errors if job does not exist.
func getJob(id string, userId uint) (*Job, error) {
j, ok := activeJobs[id]
if ok {
// Ensure user requesting a job, owns the job.
if j.UserId != userId {
slog.Warn("getJob: A user tried to access a job they do not own.", "user_id", userId, "job_id", id)
return &Job{}, errors.New("job does not exist")
}
return j, nil
}
return &Job{}, errors.New("job does not exist")
}
// Update a jobs status.
func updateJobStatus(id string, userId uint, status JobStatus) error {
j, err := getJob(id, userId)
if err != nil {
slog.Error("updateJobStatus: Failed!", "status", status, "error", err)
return err
}
j.Status = status
// If job is set to done, remove it after 1 minute.
if status == JOB_DONE {
slog.Debug("updateJobStatus: Job set to done. Will be removed after 1m.", "id", id)
go func() {
time.Sleep(1 * time.Minute)
slog.Debug("updateJobStatus: Job done. waited 1m.. removing job now.", "id", id)
rmJob(id, userId)
}()
}
return nil
}
// Update a jobs current task.
func updateJobCurrentTask(id string, userId uint, ct string) error {
j, err := getJob(id, userId)
if err != nil {
slog.Error("updateJobCurrentTask: Failed!", "ct", ct, "error", err)
return err
}
j.CurrentTask = ct
return nil
}
// Add an error to a job.
func addJobError(id string, userId uint, e string) error {
j, err := getJob(id, userId)
if err != nil {
slog.Error("updateJobCurrentTask: Failed!", "e", e, "error", err)
return err
}
j.Errors = append(j.Errors, e)
return nil
}
+20
View File
@@ -0,0 +1,20 @@
package main
import (
"log/slog"
"github.com/gin-gonic/gin"
)
// Location middleware
func WhereaboutsRequired() gin.HandlerFunc {
// TODO: This should also take into account a user
// preference (of location.. which also needs to be
// added) and server preference (env var). The US is
// good enough for now as a default.
return func(c *gin.Context) {
slog.Debug("WhereaboutsRequired middleware hit", "country", "US")
c.Set("userCountry", "US")
c.Next()
}
}
+586
View File
@@ -0,0 +1,586 @@
package main
import (
"encoding/json"
"errors"
"io"
"log/slog"
"net/http"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type PlexLoginRequest struct {
AuthToken string `json:"token" binding:"required"`
ClientIdentifier string `json:"clientIdentifier" binding:"required"`
}
type PlexUser struct {
Id uint64 `json:"id" binding:"required"`
Uuid string `json:"uuid" binding:"required"`
Email string `json:"email" binding:"required"`
Username string `json:"username" binding:"required"`
}
type PlexAccountResponse struct {
User PlexUser `json:"user" binding:"required"`
}
type PlexIdentity struct {
MediaContainer struct {
MachineIdentifier string `json:"machineIdentifier"`
} `json:"MediaContainer"`
}
type PlexHostConfigUpdateResponse struct {
PLEX_MACHINE_ID string
}
// Plex get libraries response
// /library/sections
type PlexLibrariesResponse struct {
MediaContainer struct {
Size int `json:"size"`
AllowSync bool `json:"allowSync"`
Title1 string `json:"title1"`
Directory []struct {
AllowSync bool `json:"allowSync"`
Art string `json:"art"`
Composite string `json:"composite"`
Filters bool `json:"filters"`
Refreshing bool `json:"refreshing"`
Thumb string `json:"thumb"`
Key string `json:"key"`
Type string `json:"type"`
Title string `json:"title"`
Agent string `json:"agent"`
Scanner string `json:"scanner"`
Language string `json:"language"`
UUID string `json:"uuid"`
UpdatedAt int `json:"updatedAt"`
CreatedAt int `json:"createdAt"`
ScannedAt int `json:"scannedAt"`
Content bool `json:"content"`
Directory bool `json:"directory"`
ContentChangedAt int64 `json:"contentChangedAt"`
Hidden int `json:"hidden"`
Location []struct {
ID int `json:"id"`
Path string `json:"path"`
} `json:"Location"`
} `json:"Directory"`
} `json:"MediaContainer"`
}
// Plex get all library items response (with includeGuids parameter)
// /library/sections/{sectionId}/all
type PlexLibraryItemsResponse struct {
MediaContainer struct {
Size int `json:"size"`
AllowSync bool `json:"allowSync"`
Art string `json:"art"`
Identifier string `json:"identifier"`
LibrarySectionID int `json:"librarySectionID"`
LibrarySectionTitle string `json:"librarySectionTitle"`
LibrarySectionUUID string `json:"librarySectionUUID"`
MediaTagPrefix string `json:"mediaTagPrefix"`
MediaTagVersion int `json:"mediaTagVersion"`
Thumb string `json:"thumb"`
Title1 string `json:"title1"`
Title2 string `json:"title2"`
ViewGroup string `json:"viewGroup"`
ViewMode int `json:"viewMode"`
Metadata []struct {
RatingKey string `json:"ratingKey"`
Key string `json:"key"`
GUID string `json:"guid"` // Plex guid
Slug string `json:"slug"`
Studio string `json:"studio"`
Type string `json:"type"`
Title string `json:"title"`
ContentRating string `json:"contentRating"`
Summary string `json:"summary"`
Rating float64 `json:"rating"`
UserRating float64 `json:"userRating"`
AudienceRating float64 `json:"audienceRating"`
ViewCount int `json:"viewCount,omitempty"`
LastViewedAt int64 `json:"lastViewedAt,omitempty"`
Year int `json:"year"`
Tagline string `json:"tagline"`
Thumb string `json:"thumb"`
Art string `json:"art"`
Duration int `json:"duration"`
OriginallyAvailableAt string `json:"originallyAvailableAt"`
AddedAt int `json:"addedAt"`
UpdatedAt int `json:"updatedAt"`
AudienceRatingImage string `json:"audienceRatingImage"`
HasPremiumPrimaryExtra string `json:"hasPremiumPrimaryExtra"`
RatingImage string `json:"ratingImage"`
LeafCount int `json:"leafCount,omitempty"`
ViewedLeafCount int `json:"viewedLeafCount,omitempty"`
Media []struct {
ID int `json:"id"`
Duration int `json:"duration"`
Bitrate int `json:"bitrate"`
Width int `json:"width"`
Height int `json:"height"`
AspectRatio float64 `json:"aspectRatio"`
AudioChannels int `json:"audioChannels"`
AudioCodec string `json:"audioCodec"`
VideoCodec string `json:"videoCodec"`
VideoResolution string `json:"videoResolution"`
Container string `json:"container"`
VideoFrameRate string `json:"videoFrameRate"`
VideoProfile string `json:"videoProfile"`
Part []struct {
ID int `json:"id"`
Key string `json:"key"`
Duration int `json:"duration"`
File string `json:"file"`
Size int64 `json:"size"`
Container string `json:"container"`
HasThumbnail string `json:"hasThumbnail"`
VideoProfile string `json:"videoProfile"`
} `json:"Part"`
} `json:"Media"`
// External ids
Guid []struct {
ID string `json:"id"`
} `json:"Guid"`
Genre []struct {
Tag string `json:"tag"`
} `json:"Genre"`
Country []struct {
Tag string `json:"tag"`
} `json:"Country"`
Director []struct {
Tag string `json:"tag"`
} `json:"Director"`
Writer []struct {
Tag string `json:"tag"`
} `json:"Writer"`
Role []struct {
Tag string `json:"tag"`
} `json:"Role"`
ChapterSource string `json:"chapterSource,omitempty"`
} `json:"Metadata"`
} `json:"MediaContainer"`
}
// Plex get all children (seasons) for a serie.
// /library/metadata/{ratingKey}/children
type PlexLibraryItemSeasonsResponse struct {
MediaContainer struct {
Size int `json:"size"`
AllowSync bool `json:"allowSync"`
Art string `json:"art"`
Identifier string `json:"identifier"`
Key string `json:"key"`
LibrarySectionID int `json:"librarySectionID"`
LibrarySectionTitle string `json:"librarySectionTitle"`
LibrarySectionUUID string `json:"librarySectionUUID"`
MediaTagPrefix string `json:"mediaTagPrefix"`
MediaTagVersion int `json:"mediaTagVersion"`
Nocache bool `json:"nocache"`
ParentIndex int `json:"parentIndex"`
ParentTitle string `json:"parentTitle"`
ParentYear int `json:"parentYear"`
Summary string `json:"summary"`
Theme string `json:"theme"`
Thumb string `json:"thumb"`
Title1 string `json:"title1"`
Title2 string `json:"title2"`
ViewGroup string `json:"viewGroup"`
ViewMode int `json:"viewMode"`
Directory []struct {
LeafCount int `json:"leafCount"`
Thumb string `json:"thumb"`
ViewedLeafCount int `json:"viewedLeafCount"`
Key string `json:"key"`
Title string `json:"title"`
} `json:"Directory"`
Metadata []struct {
RatingKey string `json:"ratingKey"`
Key string `json:"key"`
ParentRatingKey string `json:"parentRatingKey"`
GUID string `json:"guid"`
ParentGUID string `json:"parentGuid"`
ParentSlug string `json:"parentSlug"`
ParentStudio string `json:"parentStudio"`
Type string `json:"type"`
Title string `json:"title"`
ParentKey string `json:"parentKey"`
ParentTitle string `json:"parentTitle"`
Summary string `json:"summary"`
Index int `json:"index"`
ParentIndex int `json:"parentIndex"`
UserRating float64 `json:"userRating,omitempty"`
LastRatedAt int64 `json:"lastRatedAt,omitempty"`
ParentYear int `json:"parentYear"`
Thumb string `json:"thumb"`
Art string `json:"art"`
ParentThumb string `json:"parentThumb"`
ParentTheme string `json:"parentTheme"`
LeafCount int `json:"leafCount"`
ViewedLeafCount int `json:"viewedLeafCount"`
AddedAt int `json:"addedAt"`
UpdatedAt int `json:"updatedAt"`
ViewCount int `json:"viewCount,omitempty"`
LastViewedAt int64 `json:"lastViewedAt,omitempty"`
} `json:"Metadata"`
} `json:"MediaContainer"`
}
// Plex get all children (episodes) for a season.
// /library/metadata/{ratingKey}/allLeaves
type PlexLibraryItemEpisodesResponse struct {
MediaContainer struct {
Size int `json:"size"`
AllowSync bool `json:"allowSync"`
Art string `json:"art"`
Identifier string `json:"identifier"`
Key string `json:"key"`
LibrarySectionID int `json:"librarySectionID"`
LibrarySectionTitle string `json:"librarySectionTitle"`
LibrarySectionUUID string `json:"librarySectionUUID"`
MediaTagPrefix string `json:"mediaTagPrefix"`
MediaTagVersion int `json:"mediaTagVersion"`
MixedParents bool `json:"mixedParents"`
Nocache bool `json:"nocache"`
ParentIndex int `json:"parentIndex"`
ParentTitle string `json:"parentTitle"`
ParentYear int `json:"parentYear"`
Theme string `json:"theme"`
Title1 string `json:"title1"`
Title2 string `json:"title2"`
ViewGroup string `json:"viewGroup"`
ViewMode int `json:"viewMode"`
Metadata []struct {
RatingKey string `json:"ratingKey"`
Key string `json:"key"`
ParentRatingKey string `json:"parentRatingKey"`
GrandparentRatingKey string `json:"grandparentRatingKey"`
GUID string `json:"guid"`
GrandparentSlug string `json:"grandparentSlug"`
Studio string `json:"studio"`
Type string `json:"type"`
Title string `json:"title"`
GrandparentKey string `json:"grandparentKey"`
ParentKey string `json:"parentKey"`
GrandparentTitle string `json:"grandparentTitle"`
ParentTitle string `json:"parentTitle"`
ContentRating string `json:"contentRating"`
Summary string `json:"summary"`
Index int `json:"index"`
ParentIndex int `json:"parentIndex"`
AudienceRating float64 `json:"audienceRating"`
Year int `json:"year"`
Thumb string `json:"thumb"`
Art string `json:"art"`
ParentThumb string `json:"parentThumb"`
GrandparentThumb string `json:"grandparentThumb"`
GrandparentArt string `json:"grandparentArt"`
GrandparentTheme string `json:"grandparentTheme"`
Duration int `json:"duration"`
OriginallyAvailableAt string `json:"originallyAvailableAt"`
AddedAt int `json:"addedAt"`
UpdatedAt int `json:"updatedAt"`
AudienceRatingImage string `json:"audienceRatingImage"`
ChapterSource string `json:"chapterSource"`
TitleSort string `json:"titleSort,omitempty"`
UserRating float64 `json:"userRating,omitempty"`
ViewCount int `json:"viewCount,omitempty"`
LastViewedAt int64 `json:"lastViewedAt,omitempty"`
LastRatedAt int64 `json:"lastRatedAt,omitempty"`
} `json:"Metadata"`
} `json:"MediaContainer"`
}
// Response from clients.plex.tv/api/v2/resources
// Used to get users auth token for home plex server.
type PlexClientResources []struct {
Name string `json:"name"`
Product string `json:"product"`
ProductVersion string `json:"productVersion"`
Platform string `json:"platform"`
PlatformVersion string `json:"platformVersion"`
Device string `json:"device"`
ClientIdentifier string `json:"clientIdentifier"`
CreatedAt time.Time `json:"createdAt"`
LastSeenAt time.Time `json:"lastSeenAt"`
Provides string `json:"provides"`
OwnerID interface{} `json:"ownerId"`
SourceTitle interface{} `json:"sourceTitle"`
PublicAddress string `json:"publicAddress"`
AccessToken string `json:"accessToken"`
Owned bool `json:"owned"`
Home bool `json:"home"`
Synced bool `json:"synced"`
Relay bool `json:"relay"`
Presence bool `json:"presence"`
HTTPSRequired bool `json:"httpsRequired"`
PublicAddressMatches bool `json:"publicAddressMatches"`
DNSRebindingProtection bool `json:"dnsRebindingProtection"`
NatLoopbackSupported bool `json:"natLoopbackSupported"`
Connections []struct {
Protocol string `json:"protocol"`
Address string `json:"address"`
Port int `json:"port"`
URI string `json:"uri"`
Local bool `json:"local"`
Relay bool `json:"relay"`
IPv6 bool `json:"IPv6"`
} `json:"connections"`
}
// Plex access middleware, ensures user is a Plex user.
// To be ran after AuthRequired middleware with extra data.
func PlexAccessRequired(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
slog.Debug("PlexAccessRequired middleware hit", "user_id", userId)
userType := c.MustGet("userType").(UserType)
if Config.PLEX_HOST == "" || Config.PLEX_MACHINE_ID == "" {
slog.Error("PlexAccessRequired: Plex has not been configured.", "user_id", userId)
c.AbortWithStatus(401)
return
}
if userType != PLEX_USER {
slog.Error("PlexAccessRequired: User is not a Plex user..", "user_id", userId, "user_type", userType)
c.AbortWithStatus(401)
return
}
userPlexService := new(UserServices)
if res := db.Where("user_id = ? AND name = ?", userId, "plex").Take(&userPlexService); res.Error != nil {
slog.Error("PlexAccessRequired: Failed when attempting to get users plex service integration..", "user_id", userId, "user_type", userType)
c.AbortWithStatus(401)
return
}
if userPlexService.ClientID == "" || userPlexService.AuthToken == "" || userPlexService.AuthToken2 == "" {
slog.Error("PlexAccessRequired: User has missing details from service (clientId, authToken or authToken2)..", "user_id", userId, "client_id", userPlexService.ClientID)
c.AbortWithStatus(401)
return
}
c.Set("plexAuthToken", userPlexService.AuthToken)
c.Set("plexLocalAuthToken", userPlexService.AuthToken2)
}
}
func getPlexIdentity(host string) (PlexIdentity, error) {
httpClient := &http.Client{}
req, err := http.NewRequest("GET", host+"/identity", nil)
if err != nil {
return PlexIdentity{}, err
}
req.Header.Set("Accept", "application/json")
resp, err := httpClient.Do(req)
if err != nil {
return PlexIdentity{}, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return PlexIdentity{}, err
}
defer resp.Body.Close()
var pi PlexIdentity
err = json.Unmarshal(body, &pi)
if err != nil {
return PlexIdentity{}, err
}
return pi, nil
}
func fetchPlexAccountFromToken(token string) (PlexUser, error) {
httpClient := &http.Client{}
req, err := http.NewRequest("GET", "https://plex.tv/users/account.json", nil)
if err != nil {
return PlexUser{}, err
}
req.Header.Set("X-Plex-Token", token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := httpClient.Do(req)
if err != nil {
return PlexUser{}, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return PlexUser{}, err
}
defer resp.Body.Close()
var pa PlexAccountResponse
err = json.Unmarshal(body, &pa)
if err != nil {
return PlexUser{}, err
}
return pa.User, nil
}
// Update plex host setting
func updateConfigPlexHost(v string) (PlexHostConfigUpdateResponse, error) {
Config.PLEX_HOST = v
if Config.PLEX_HOST != "" {
pi, err := getPlexIdentity(Config.PLEX_HOST)
if err != nil {
slog.Error("updateConfigPlexHost: Failed to get plex server identity!", "error", err)
return PlexHostConfigUpdateResponse{}, errors.New("failed to get Plex server identity. Please try setting the Plex Host again or setting PLEX_MACHINE_ID manually in your config file")
}
if pi.MediaContainer.MachineIdentifier == "" {
slog.Error("updateConfigPlexHost: Plex server identity response had no machine id!", "response", pi)
return PlexHostConfigUpdateResponse{}, errors.New("got Plex server identity, but no machine id was found")
}
Config.PLEX_MACHINE_ID = pi.MediaContainer.MachineIdentifier
} else {
Config.PLEX_MACHINE_ID = ""
}
if err := writeConfig(); err != nil {
slog.Error("updateConfigPlexHost: Failed to write updated config to file!", "err", err)
return PlexHostConfigUpdateResponse{}, errors.New("failed to write config")
}
return PlexHostConfigUpdateResponse{PLEX_MACHINE_ID: Config.PLEX_MACHINE_ID}, nil
}
func getPlexLibraries(plexAuth string) (PlexLibrariesResponse, error) {
httpClient := &http.Client{}
req, err := http.NewRequest("GET", Config.PLEX_HOST+"/library/sections", nil)
if err != nil {
return PlexLibrariesResponse{}, err
}
req.Header.Add("Accept", "application/json")
req.Header.Set("X-Plex-Token", plexAuth)
resp, err := httpClient.Do(req)
if err != nil {
return PlexLibrariesResponse{}, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return PlexLibrariesResponse{}, err
}
defer resp.Body.Close()
var pl PlexLibrariesResponse
err = json.Unmarshal(body, &pl)
if err != nil {
return PlexLibrariesResponse{}, err
}
return pl, nil
}
func getPlexLibraryItems(plexAuth string, libraryKey string) (PlexLibraryItemsResponse, error) {
httpClient := &http.Client{}
req, err := http.NewRequest("GET", Config.PLEX_HOST+"/library/sections/"+libraryKey+"/all?includeGuids=1", nil)
if err != nil {
return PlexLibraryItemsResponse{}, err
}
req.Header.Add("Accept", "application/json")
req.Header.Set("X-Plex-Token", plexAuth)
resp, err := httpClient.Do(req)
if err != nil {
return PlexLibraryItemsResponse{}, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return PlexLibraryItemsResponse{}, err
}
defer resp.Body.Close()
var pl PlexLibraryItemsResponse
err = json.Unmarshal(body, &pl)
if err != nil {
return PlexLibraryItemsResponse{}, err
}
return pl, nil
}
func getPlexLibraryItemSeasons(plexAuth string, ratingKey string) (PlexLibraryItemSeasonsResponse, error) {
httpClient := &http.Client{}
req, err := http.NewRequest("GET", Config.PLEX_HOST+"/library/metadata/"+ratingKey+"/children", nil)
if err != nil {
return PlexLibraryItemSeasonsResponse{}, err
}
req.Header.Add("Accept", "application/json")
req.Header.Set("X-Plex-Token", plexAuth)
resp, err := httpClient.Do(req)
if err != nil {
return PlexLibraryItemSeasonsResponse{}, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return PlexLibraryItemSeasonsResponse{}, err
}
defer resp.Body.Close()
var pl PlexLibraryItemSeasonsResponse
err = json.Unmarshal(body, &pl)
if err != nil {
return PlexLibraryItemSeasonsResponse{}, err
}
return pl, nil
}
func getPlexLibraryItemEpisodes(plexAuth string, ratingKey string) (PlexLibraryItemEpisodesResponse, error) {
httpClient := &http.Client{}
req, err := http.NewRequest("GET", Config.PLEX_HOST+"/library/metadata/"+ratingKey+"/allLeaves", nil)
if err != nil {
return PlexLibraryItemEpisodesResponse{}, err
}
req.Header.Add("Accept", "application/json")
req.Header.Set("X-Plex-Token", plexAuth)
resp, err := httpClient.Do(req)
if err != nil {
return PlexLibraryItemEpisodesResponse{}, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return PlexLibraryItemEpisodesResponse{}, err
}
defer resp.Body.Close()
var pl PlexLibraryItemEpisodesResponse
err = json.Unmarshal(body, &pl)
if err != nil {
return PlexLibraryItemEpisodesResponse{}, err
}
return pl, nil
}
// Gets users auth token for local plex server,
// so they can authenticate against it for api requests.
// If no auth token is returned or errored, assume user doesn't have access to home plex server library.
func getPlexHomeServerAuthToken(plexAuth string, userClientId string) (string, error) {
httpClient := &http.Client{}
req, err := http.NewRequest("GET", "https://clients.plex.tv/api/v2/resources", nil)
if err != nil {
return "", err
}
req.Header.Add("Accept", "application/json")
req.Header.Set("X-Plex-Token", plexAuth)
req.Header.Set("X-Plex-Client-Identifier", userClientId)
resp, err := httpClient.Do(req)
if err != nil {
return "", err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
defer resp.Body.Close()
var pl PlexClientResources
err = json.Unmarshal(body, &pl)
if err != nil {
return "", err
}
authToken := ""
for _, v := range pl {
if v.ClientIdentifier == Config.PLEX_MACHINE_ID {
slog.Debug("getPlexHomeServerAuthToken: Found entry with clientIdentifier matching home server machine id.")
if v.AccessToken == "" {
slog.Error("getPlexHomeServerAuthToken: Matching entry has no AccessToken!")
continue
}
authToken = v.AccessToken
}
}
if authToken == "" {
slog.Error("getPlexHomeServerAuthToken: No authToken retrieved!")
}
return authToken, nil
}
+267
View File
@@ -0,0 +1,267 @@
package main
import (
"errors"
"log/slog"
"strconv"
"strings"
"time"
"gorm.io/gorm"
)
type PlexSyncResponse struct {
JobId string `json:"jobId"`
}
// Perform a Plex sync.
// Errors are added silently to the job.
func startPlexSync(
db *gorm.DB,
jobId string,
userId uint,
userPlexLocalAuth string,
) {
updateJobCurrentTask(jobId, userId, "fetching libraries")
libraries, err := getPlexLibraries(userPlexLocalAuth)
if err != nil {
slog.Error("plexSyncWatched: Failed to fetch libraries", "user_id", userId, "error", err)
addJobError(jobId, userId, "failed to get plex libraries")
updateJobStatus(jobId, userId, JOB_DONE)
return
}
for _, library := range libraries.MediaContainer.Directory {
slog.Debug("plexSyncWatched: Processing a library", "library_title", library.Title, "library_type", library.Type, "user_id", userId)
if library.Type == "movie" {
updateJobCurrentTask(jobId, userId, "importing movies from "+library.Title)
movies, err := getPlexLibraryItems(userPlexLocalAuth, library.Key)
if err != nil {
slog.Error("plexSyncWatched: Failed to fetch movies from library", "library", library.Key, "user_id", userId, "error", err)
addJobError(jobId, userId, "failed to fetch movies from library "+library.Key)
continue
}
for _, movie := range movies.MediaContainer.Metadata {
if movie.ViewCount == 0 {
// Not viewed and not rated, skip importing
slog.Debug("plexSyncWatched: Skipping unwatched movie:", "movie_name", movie.Title, "user_id", userId)
continue
}
updateJobCurrentTask(jobId, userId, "importing movie "+movie.Title)
slog.Info("plexSyncWatched: Importing movie.", "movie_name", movie.Title, "user_id", userId)
// Find tmdb id
if len(movie.Guid) <= 0 {
slog.Error("plexSyncWatched: Movie to import does not have any external guids.", "movie_name", movie.Title, "movie_id", movie.GUID, "user_id", userId)
addJobError(jobId, userId, "movie could not be imported (no external ids present): "+movie.Title)
continue
}
tmdbIdStr := ""
for _, v := range movie.Guid {
if strings.HasPrefix(v.ID, "tmdb://") {
tmdbIdStr = v.ID[7:]
break
}
}
if tmdbIdStr == "" {
slog.Error("plexSyncWatched: Movie to import does not have a tmdb id.", "movie_name", movie.Title, "tmdb_id_str", tmdbIdStr, "movie_id", movie.GUID, "user_id", userId)
addJobError(jobId, userId, "movie could not be imported (no tmdbId present): "+movie.Title)
continue
}
tmdbId, err := strconv.Atoi(tmdbIdStr)
if err != nil {
slog.Error("plexSyncWatched: Movie to import does not have a parseable (to int) tmdb id.", "movie_name", movie.Title, "tmdb_id_str", tmdbIdStr, "movie_id", movie.GUID, "user_id", userId)
addJobError(jobId, userId, "movie could not be imported (tmdbId was not parseable): "+movie.Title)
continue
}
lastViewedAt := time.Unix(movie.LastViewedAt, 0)
w, err := addWatched(db, userId, WatchedAddRequest{
Status: FINISHED,
ContentID: tmdbId,
ContentType: MOVIE,
Rating: int8(movie.UserRating),
WatchedDate: lastViewedAt,
}, IMPORTED_WATCHED_PLEX)
if err != nil {
if err.Error() == "content already on watched list" {
slog.Error("plexSyncWatched: unique constraint hit. movie must already be on watch list", "error", err)
continue
}
slog.Error("plexSyncWatched: Failed to add movie as watched", "error", err)
addJobError(jobId, userId, "failed to add movie "+movie.Title)
} else {
// 3. Add IMPORTED_ADDED_WATCHED_PLEX activity
if !lastViewedAt.IsZero() {
_, err := addActivity(db, userId, ActivityAddRequest{
WatchedID: w.ID,
Type: IMPORTED_ADDED_WATCHED_PLEX,
CustomDate: &lastViewedAt,
})
if err != nil {
slog.Error("plexSyncWatched: Failed to add dateswatched activity.", "movie_name", movie.Title,
"movie_id", movie.GUID, "user_id", userId, "date", lastViewedAt, "unparsed_date", movie.LastViewedAt, "error", err)
}
}
}
}
} else if library.Type == "show" {
updateJobCurrentTask(jobId, userId, "importing tv shows from "+library.Title)
shows, err := getPlexLibraryItems(userPlexLocalAuth, library.Key)
if err != nil {
slog.Error("plexSyncWatched: Failed to fetch shows from library", "library", library.Key, "error", err)
addJobError(jobId, userId, "failed to fetch shows from library "+library.Key)
continue
}
for _, show := range shows.MediaContainer.Metadata {
if show.ViewedLeafCount != show.LeafCount {
// Not viewed, skip importing
// (could be improved to set status as watching when viewedLeafCount is higher than 0)
slog.Debug("plexSyncWatched: Skipping unwatched show:", "show_name", show.Title, "leaf_count", show.LeafCount, "viewed_leaf_count", show.ViewedLeafCount, "user_id", userId)
continue
}
updateJobCurrentTask(jobId, userId, "importing show "+show.Title)
slog.Info("plexSyncWatched: Importing show.", "show_name", show.Title, "user_id", userId)
tmdbIdStr := ""
for _, v := range show.Guid {
if strings.HasPrefix(v.ID, "tmdb://") {
tmdbIdStr = v.ID[7:]
break
}
}
if tmdbIdStr == "" {
slog.Error("plexSyncWatched: Show to import does not have a tmdb id.", "show_name", show.Title, "tmdb_id_str", tmdbIdStr, "show_id", show.GUID, "user_id", userId)
addJobError(jobId, userId, "movie could not be imported (no tmdbId present): "+show.Title)
continue
}
tmdbId, err := strconv.Atoi(tmdbIdStr)
if err != nil {
slog.Error("plexSyncWatched: Show to import does not have a parseable (to int) tmdb id.", "show_name", show.Title, "tmdb_id_str", tmdbIdStr, "show_id", show.GUID, "user_id", userId)
addJobError(jobId, userId, "show could not be imported (tmdbId was not parseable): "+show.Title)
continue
}
lastViewedAt := time.Unix(show.LastViewedAt, 0)
w, err := addWatched(db, userId, WatchedAddRequest{
Status: FINISHED,
ContentID: tmdbId,
ContentType: SHOW,
Rating: int8(show.UserRating),
WatchedDate: lastViewedAt,
}, IMPORTED_WATCHED_PLEX)
if err != nil {
if err.Error() == "content already on watched list" {
slog.Info("plexSyncWatched: unique constraint hit. show must already be on watch list", "error", err)
} else {
slog.Error("plexSyncWatched: Failed to add show as watched", "error", err)
addJobError(jobId, userId, "failed to add show "+show.Title)
}
} else {
// 3. Add IMPORTED_ADDED_WATCHED_PLEX activity
if !lastViewedAt.IsZero() {
_, err := addActivity(db, userId, ActivityAddRequest{
WatchedID: w.ID,
Type: IMPORTED_ADDED_WATCHED_PLEX,
CustomDate: &lastViewedAt,
})
if err != nil {
slog.Error("plexSyncWatched: Failed to add dateswatched activity.", "movie_name", show.Title,
"movie_id", show.GUID, "user_id", userId, "date", lastViewedAt, "unparsed_date", show.LastViewedAt, "error", err)
}
}
}
// Import watched seasons for this serie
seriesSeasons, err := getPlexLibraryItemSeasons(userPlexLocalAuth, show.RatingKey)
if err != nil {
slog.Error("plexSyncWatched: Failed to fetch series seasons.", "series_name", show.Title, "series_id", show.GUID, "user_id", userId, "error", err)
addJobError(jobId, userId, "series seasons could not be imported (request failed): "+show.Title)
} else if len(seriesSeasons.MediaContainer.Metadata) <= 0 {
slog.Info("plexSyncWatched: Series has no seasons.", "series_name", show.Title, "serie_ids", show.GUID, "user_id", userId)
} else {
for _, vs := range seriesSeasons.MediaContainer.Metadata {
slog.Debug("plexSyncWatched: Processing a season.", "full_item", vs, "user_id", userId)
if vs.ViewedLeafCount != vs.LeafCount {
slog.Debug("plexSyncWatched: Skipping import of unplayed season.", "series_name", show.Title, "season_num", vs.Index, "user_id", userId)
continue
}
updateJobCurrentTask(jobId, userId, "syncing "+show.Title+" season "+strconv.Itoa(vs.Index))
var seasonLastViewedAt time.Time
if vs.LastViewedAt != 0 {
seasonLastViewedAt = time.Unix(vs.LastViewedAt, 0)
}
_, err = addWatchedSeason(db, userId, WatchedSeasonAddRequest{
WatchedID: w.ID,
SeasonNumber: vs.Index,
Status: FINISHED,
addActivity: SEASON_ADDED_PLEX,
addActivityDate: seasonLastViewedAt,
})
if err != nil {
slog.Error("plexSyncWatched: Failed to fetch series seasons.", "series_name", show.Title, "series_id", show.GUID, "user_id", userId, "error", err)
addJobError(jobId, userId, "series season could not be imported (addWatchedSeason request failed): "+show.Title+" season "+strconv.Itoa(vs.Index))
}
}
}
// Import watched episodes for this serie
seriesEpisodes, err := getPlexLibraryItemEpisodes(userPlexLocalAuth, show.RatingKey)
if err != nil {
slog.Error("plexSyncWatched: Failed to fetch series episodes.", "series_name", show.Title, "series_id", show.GUID, "user_id", userId, "error", err)
addJobError(jobId, userId, "series episodes could not be imported (request failed): "+show.Title)
} else if len(seriesEpisodes.MediaContainer.Metadata) <= 0 {
slog.Info("plexSyncWatched: Series has no episodes.", "series_name", show.Title, "series_id", show.GUID, "user_id", userId)
} else {
for _, vs := range seriesEpisodes.MediaContainer.Metadata {
slog.Debug("plexSyncWatched: Processing an episode.", "full_item", vs, "user_id", userId)
if vs.ViewCount <= 0 {
slog.Debug("plexSyncWatched: Skipping import of unplayed episode.", "series_name", show.Title, "season_num", vs.ParentIndex, "episode_num", vs.Index, "user_id", userId)
continue
}
updateJobCurrentTask(jobId, userId, "syncing "+show.Title+" season "+strconv.Itoa(vs.ParentIndex)+" episode "+strconv.Itoa(vs.Index))
var episodeLastViewedAt time.Time
if vs.LastViewedAt != 0 {
episodeLastViewedAt = time.Unix(vs.LastViewedAt, 0)
}
_, err = addWatchedEpisodes(db, userId, WatchedEpisodeAddRequest{
WatchedID: w.ID,
SeasonNumber: vs.ParentIndex,
EpisodeNumber: vs.Index,
Status: FINISHED,
addActivity: EPISODE_ADDED_PLEX,
addActivityDate: episodeLastViewedAt,
})
if err != nil {
slog.Error("plexSyncWatched: Failed to import series episode.", "series_name", show.Title, "season_num", vs.ParentIndex, "episode_num", vs.Index, "user_id", userId, "error", err)
addJobError(jobId, userId, "series episode could not be imported (addWatchedEpisode request failed): "+show.Title+" "+vs.Title)
}
}
}
}
}
}
updateJobStatus(jobId, userId, JOB_DONE)
}
func plexSyncWatched(
db *gorm.DB,
userId uint,
userPlexLocalAuth string,
) (PlexSyncResponse, error) {
jobId, err := addJob("plex_sync", userId)
if err != nil {
slog.Error("startPlexSync: Failed to create a job", "error", err)
return PlexSyncResponse{}, errors.New("failed to create job")
}
updateJobStatus(jobId, userId, JOB_RUNNING)
go startPlexSync(
db,
jobId,
userId,
userPlexLocalAuth,
)
return PlexSyncResponse{JobId: jobId}, nil
}
+124
View File
@@ -0,0 +1,124 @@
package main
import (
"encoding/json"
"errors"
"log/slog"
"time"
"gorm.io/gorm"
)
type Profile struct {
Joined time.Time `json:"joined"`
ShowsWatched int32 `json:"showsWatched"`
MoviesWatched int32 `json:"moviesWatched"`
MoviesWatchedRuntime uint32 `json:"moviesWatchedRuntime"`
ShowsWatchedRuntime uint32 `json:"showsWatchedRuntime"`
}
// Check if content has been previsouly watched by looking for related activity.
func hasBeenPreviouslyWatched(a *[]Activity) bool {
wp := false
var relatedActivity []Activity
for _, v := range *a {
if v.Type == ADDED_WATCHED ||
v.Type == IMPORTED_ADDED_WATCHED ||
v.Type == IMPORTED_WATCHED ||
v.Type == STATUS_CHANGED {
relatedActivity = append(relatedActivity, v)
}
}
if len(relatedActivity) <= 0 {
return false
}
for _, ra := range relatedActivity {
if ra.Type == IMPORTED_ADDED_WATCHED {
wp = true
break
} else if ra.Type == ADDED_WATCHED || ra.Type == IMPORTED_WATCHED {
if ra.Data == "" {
continue
}
var v map[string]any
err := json.Unmarshal([]byte(ra.Data), &v)
if err != nil {
slog.Error("Checking ADDED_WATCHED or IMPORTED_WATCHED.. failed to parse json data", "error", err)
continue
}
if status, ok := v["status"]; ok {
if status == "FINISHED" {
wp = true
break
}
}
} else if ra.Type == STATUS_CHANGED {
if ra.Data == "FINISHED" {
wp = true
break
}
}
}
return wp
}
// Gets any data required for profile page
func getProfile(db *gorm.DB, userId uint) (Profile, error) {
user := new(User)
res := db.Model(&User{}).Where("id = ?", userId).Take(&user)
if res.Error != nil {
slog.Error("Failed to get profile:", "error", res.Error.Error())
return Profile{}, errors.New("failed to get profile")
}
watched := new([]Watched)
res = db.Model(&Watched{}).Preload("Content").Preload("Activity").Where("user_id = ?", userId).Find(&watched)
if res.Error != nil {
slog.Error("Profile: Failed to get watched for processing:", "error", res.Error.Error())
return Profile{}, errors.New("failed to get watched for processing")
}
var (
showsWatched int32
moviesWatched int32
moviesWatchedRuntime uint32
showsWatchedRuntime uint32
)
for _, w := range *watched {
isFinished := false
if w.Status == FINISHED {
isFinished = true
} else if *user.IncludePreviouslyWatched && hasBeenPreviouslyWatched(&w.Activity) {
// If status is not finished and user has IncludePreviouslyWatched enabled,
// then we can also check if content hasBeenPreviouslyWatched.
isFinished = true
}
if isFinished {
if w.Content == nil {
continue
}
c := *w.Content
if c.Type == SHOW {
showsWatched++
// This aint a science, just a very inaccurate guesstimate.
if c.NumberOfEpisodes != 0 {
var showRuntime uint32 = 30
if c.Runtime != 0 {
showRuntime = c.Runtime
}
showsWatchedRuntime += showRuntime * c.NumberOfEpisodes
slog.Debug("calcualted", "show", c.Title, "runti", showRuntime*c.NumberOfEpisodes)
}
} else if c.Type == MOVIE {
moviesWatched++
moviesWatchedRuntime += c.Runtime
}
}
}
profile := Profile{
Joined: user.CreatedAt,
ShowsWatched: showsWatched,
MoviesWatched: moviesWatched,
MoviesWatchedRuntime: moviesWatchedRuntime,
ShowsWatchedRuntime: showsWatchedRuntime,
}
return profile, nil
}
+978 -33
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
package main
import (
"time"
"gorm.io/gorm"
)
// Setup recurring tasks (eg cleanup every x mins)
func setupTasks(db *gorm.DB) {
go setupMinutelyTasks(db)
go setupDailyTasks(db)
}
func setupMinutelyTasks(db *gorm.DB) {
taskRunInterval := 1 * time.Minute
ticker := time.NewTicker(taskRunInterval)
defer ticker.Stop()
for range ticker.C {
// Runs funcs that are in the place where we are cleaning.
// Bit cleaner and we can keep the related code close to its home.
cleanupTokens(db)
}
}
func setupDailyTasks(db *gorm.DB) {
taskRunInterval := 24 * time.Hour
ticker := time.NewTicker(taskRunInterval)
defer ticker.Stop()
for range ticker.C {
cleanupImages(db)
}
}
+300 -16
View File
@@ -4,8 +4,10 @@ import (
"encoding/json"
"errors"
"io"
"log/slog"
"net/http"
"net/url"
"time"
)
type TMDBSearchMultiResponse struct {
@@ -27,11 +29,11 @@ type TMDBSearchMultiResults struct {
ProfilePath string `json:"profile_path"`
MediaType string `json:"media_type"`
GenreIds []int64 `json:"genre_ids"`
Popularity float64 `json:"popularity"`
Popularity float32 `json:"popularity"`
ReleaseDate string `json:"release_date,omitempty"`
Video bool `json:"video,omitempty"`
VoteAverage float64 `json:"vote_average"`
VoteCount int `json:"vote_count"`
VoteAverage float32 `json:"vote_average"`
VoteCount uint32 `json:"vote_count"`
Name string `json:"name,omitempty"`
OriginalName string `json:"original_name,omitempty"`
FirstAirDate string `json:"first_air_date,omitempty"`
@@ -47,7 +49,7 @@ type TMDBContentDetails struct {
} `json:"genres"`
PosterPath string `json:"poster_path"`
Homepage string `json:"homepage"`
Popularity float64 `json:"popularity"`
Popularity float32 `json:"popularity"`
Overview string `json:"overview"`
OriginalLanguage string `json:"original_language"`
ProductionCompanies []struct {
@@ -62,8 +64,8 @@ type TMDBContentDetails struct {
} `json:"production_countries"`
Status string `json:"status"`
Tagline string `json:"tagline"`
VoteAverage float64 `json:"vote_average"`
VoteCount int `json:"vote_count"`
VoteAverage float32 `json:"vote_average"`
VoteCount uint32 `json:"vote_count"`
SpokenLanguages []struct {
EnglishName string `json:"english_name"`
Iso6391 string `json:"iso_639_1"`
@@ -75,14 +77,20 @@ type TMDBMovieDetails struct {
TMDBContentDetails
Adult bool `json:"adult"`
BelongsToCollection any `json:"belongs_to_collection"`
Budget int `json:"budget"`
Budget uint32 `json:"budget"`
ImdbID string `json:"imdb_id"`
OriginalTitle string `json:"original_title"`
ReleaseDate string `json:"release_date"`
Revenue int `json:"revenue"`
Runtime int `json:"runtime"`
Revenue uint32 `json:"revenue"`
Runtime uint32 `json:"runtime"`
Title string `json:"title"`
Video bool `json:"video"`
// Extra items because we use `append_to_response` on the request
Videos TMDBContentVideos `json:"videos"`
WatchProviders interface{} `json:"watch/providers"`
Similar TMDBMovieSimilar `json:"similar"`
ExternalIds TMDBExternalIdsMovie `json:"external_ids"`
}
type TMDBShowDetails struct {
@@ -109,7 +117,7 @@ type TMDBShowDetails struct {
SeasonNumber int `json:"season_number"`
StillPath string `json:"still_path"`
VoteAverage float64 `json:"vote_average"`
VoteCount int `json:"vote_count"`
VoteCount uint32 `json:"vote_count"`
} `json:"last_episode_to_air"`
Name string `json:"name"`
NextEpisodeToAir any `json:"next_episode_to_air"`
@@ -119,8 +127,8 @@ type TMDBShowDetails struct {
LogoPath string `json:"logo_path"`
OriginCountry string `json:"origin_country"`
} `json:"networks"`
NumberOfEpisodes int `json:"number_of_episodes"`
NumberOfSeasons int `json:"number_of_seasons"`
NumberOfEpisodes uint32 `json:"number_of_episodes"`
NumberOfSeasons uint32 `json:"number_of_seasons"`
OriginCountry []string `json:"origin_country"`
OriginalName string `json:"original_name"`
Seasons []struct {
@@ -133,6 +141,129 @@ type TMDBShowDetails struct {
SeasonNumber int `json:"season_number"`
} `json:"seasons"`
Type string `json:"type"`
// Extra items because we use `append_to_response` on the request
Videos TMDBContentVideos `json:"videos"`
WatchProviders interface{} `json:"watch/providers"`
Similar TMDBShowSimilar `json:"similar"`
ExternalIds TMDBExternalIdsShow `json:"external_ids"`
Keywords TMDBKeywords `json:"keywords"`
}
type WatchProvider struct {
ProviderID int `json:"provider_id"`
ProviderName string `json:"provider_name"`
DisplayPriority int `json:"display_priority"`
}
type TMDBContentVideos struct {
ID int `json:"id"`
Results []struct {
Iso6391 string `json:"iso_639_1"`
Iso31661 string `json:"iso_3166_1"`
Name string `json:"name"`
Key string `json:"key"`
Site string `json:"site"`
Size int `json:"size"`
Type string `json:"type"`
Official bool `json:"official"`
PublishedAt time.Time `json:"published_at"`
ID string `json:"id"`
} `json:"results"`
}
type TMDBSeasonDetails struct {
ID string `json:"_id"`
AirDate string `json:"air_date"`
Episodes []struct {
AirDate string `json:"air_date"`
EpisodeNumber int `json:"episode_number"`
ID int `json:"id"`
Name string `json:"name"`
Overview string `json:"overview"`
ProductionCode string `json:"production_code"`
Runtime int `json:"runtime"`
SeasonNumber int `json:"season_number"`
ShowID int `json:"show_id"`
StillPath string `json:"still_path"`
VoteAverage float64 `json:"vote_average"`
VoteCount int `json:"vote_count"`
Crew []struct {
Department string `json:"department"`
Job string `json:"job"`
CreditID string `json:"credit_id"`
Adult bool `json:"adult"`
Gender int `json:"gender"`
ID int `json:"id"`
KnownForDepartment string `json:"known_for_department"`
Name string `json:"name"`
OriginalName string `json:"original_name"`
Popularity float64 `json:"popularity"`
ProfilePath string `json:"profile_path"`
} `json:"crew"`
GuestStars []struct {
Character string `json:"character"`
CreditID string `json:"credit_id"`
Order int `json:"order"`
Adult bool `json:"adult"`
Gender int `json:"gender"`
ID int `json:"id"`
KnownForDepartment string `json:"known_for_department"`
Name string `json:"name"`
OriginalName string `json:"original_name"`
Popularity float64 `json:"popularity"`
ProfilePath string `json:"profile_path"`
} `json:"guest_stars"`
} `json:"episodes"`
Name string `json:"name"`
Overview string `json:"overview"`
ID0 int `json:"id"`
PosterPath string `json:"poster_path"`
SeasonNumber int `json:"season_number"`
}
type TMDBShowSimilar struct {
Page int `json:"page"`
Results []struct {
Adult bool `json:"adult"`
BackdropPath string `json:"backdrop_path"`
GenreIds []int `json:"genre_ids"`
ID int `json:"id"`
OriginCountry []string `json:"origin_country"`
OriginalLanguage string `json:"original_language"`
OriginalName string `json:"original_name"`
Overview string `json:"overview"`
Popularity float64 `json:"popularity"`
PosterPath string `json:"poster_path"`
FirstAirDate string `json:"first_air_date"`
Name string `json:"name"`
VoteAverage float64 `json:"vote_average"`
VoteCount uint32 `json:"vote_count"`
} `json:"results"`
TotalPages int `json:"total_pages"`
TotalResults int `json:"total_results"`
}
type TMDBMovieSimilar struct {
Page int `json:"page"`
Results []struct {
Adult bool `json:"adult"`
BackdropPath string `json:"backdrop_path"`
GenreIds []int `json:"genre_ids"`
ID int `json:"id"`
OriginalLanguage string `json:"original_language"`
OriginalTitle string `json:"original_title"`
Overview string `json:"overview"`
Popularity float64 `json:"popularity"`
PosterPath string `json:"poster_path"`
ReleaseDate string `json:"release_date"`
Title string `json:"title"`
Video bool `json:"video"`
VoteAverage float64 `json:"vote_average"`
VoteCount uint32 `json:"vote_count"`
} `json:"results"`
TotalPages int `json:"total_pages"`
TotalResults int `json:"total_results"`
}
type TMDBPersonDetails struct {
@@ -171,7 +302,7 @@ type TMDBPersonCombinedCreditsCast struct {
PosterPath string `json:"poster_path"`
FirstAirDate string `json:"first_air_date"`
VoteAverage float64 `json:"vote_average"`
VoteCount int `json:"vote_count"`
VoteCount uint32 `json:"vote_count"`
Character string `json:"character"`
BackdropPath string `json:"backdrop_path"`
Popularity float64 `json:"popularity"`
@@ -214,8 +345,161 @@ type TMDBContentCredits struct {
} `json:"crew"`
}
type TMDBDiscoverMovies struct {
Page int `json:"page"`
Results []struct {
Adult bool `json:"adult"`
BackdropPath string `json:"backdrop_path"`
GenreIds []int `json:"genre_ids"`
ID int `json:"id"`
OriginalLanguage string `json:"original_language"`
OriginalTitle string `json:"original_title"`
Overview string `json:"overview"`
Popularity float64 `json:"popularity"`
PosterPath string `json:"poster_path"`
ReleaseDate string `json:"release_date"`
Title string `json:"title"`
Video bool `json:"video"`
VoteAverage float64 `json:"vote_average"`
VoteCount int `json:"vote_count"`
} `json:"results"`
TotalPages int `json:"total_pages"`
TotalResults int `json:"total_results"`
}
type TMDBDiscoverShows struct {
Page int `json:"page"`
Results []struct {
BackdropPath string `json:"backdrop_path"`
FirstAirDate string `json:"first_air_date"`
GenreIds []int `json:"genre_ids"`
ID int `json:"id"`
Name string `json:"name"`
OriginCountry []string `json:"origin_country"`
OriginalLanguage string `json:"original_language"`
OriginalName string `json:"original_name"`
Overview string `json:"overview"`
Popularity float64 `json:"popularity"`
PosterPath string `json:"poster_path"`
VoteAverage float32 `json:"vote_average"`
VoteCount int `json:"vote_count"`
} `json:"results"`
TotalPages int `json:"total_pages"`
TotalResults int `json:"total_results"`
}
type TMDBTrendingAll struct {
Page int `json:"page"`
Results []struct {
Adult bool `json:"adult"`
BackdropPath string `json:"backdrop_path"`
ID int `json:"id"`
Title string `json:"title,omitempty"`
OriginalLanguage string `json:"original_language"`
OriginalTitle string `json:"original_title,omitempty"`
Overview string `json:"overview"`
PosterPath string `json:"poster_path"`
MediaType string `json:"media_type"`
GenreIds []int `json:"genre_ids"`
Popularity float64 `json:"popularity"`
ReleaseDate string `json:"release_date,omitempty"`
Video bool `json:"video,omitempty"`
VoteAverage float64 `json:"vote_average"`
VoteCount int `json:"vote_count"`
Name string `json:"name,omitempty"`
OriginalName string `json:"original_name,omitempty"`
FirstAirDate string `json:"first_air_date,omitempty"`
OriginCountry []string `json:"origin_country,omitempty"`
} `json:"results"`
TotalPages int `json:"total_pages"`
TotalResults int `json:"total_results"`
}
type TMDBUpcomingMovies struct {
Dates struct {
Maximum string `json:"maximum"`
Minimum string `json:"minimum"`
} `json:"dates"`
Page int `json:"page"`
Results []struct {
Adult bool `json:"adult"`
BackdropPath string `json:"backdrop_path"`
GenreIds []int `json:"genre_ids"`
ID int `json:"id"`
OriginalLanguage string `json:"original_language"`
OriginalTitle string `json:"original_title"`
Overview string `json:"overview"`
Popularity float64 `json:"popularity"`
PosterPath string `json:"poster_path"`
ReleaseDate string `json:"release_date"`
Title string `json:"title"`
Video bool `json:"video"`
VoteAverage float32 `json:"vote_average"`
VoteCount int `json:"vote_count"`
} `json:"results"`
TotalPages int `json:"total_pages"`
TotalResults int `json:"total_results"`
}
type TMDBUpcomingShows struct {
Page int `json:"page"`
Results []struct {
BackdropPath string `json:"backdrop_path"`
FirstAirDate string `json:"first_air_date"`
GenreIds []int `json:"genre_ids"`
ID int `json:"id"`
Name string `json:"name"`
OriginCountry []string `json:"origin_country"`
OriginalLanguage string `json:"original_language"`
OriginalName string `json:"original_name"`
Overview string `json:"overview"`
Popularity float64 `json:"popularity"`
PosterPath string `json:"poster_path"`
VoteAverage float32 `json:"vote_average"`
VoteCount int `json:"vote_count"`
} `json:"results"`
TotalPages int `json:"total_pages"`
TotalResults int `json:"total_results"`
}
type TMDBExternalIds struct {
ID int `json:"id"`
ImdbID string `json:"imdb_id"`
WikidataID string `json:"wikidata_id"`
FacebookID string `json:"facebook_id"`
InstagramID string `json:"instagram_id"`
TwitterID string `json:"twitter_id"`
}
type TMDBExternalIdsMovie struct {
TMDBExternalIds
}
type TMDBExternalIdsShow struct {
TMDBExternalIds
FreebaseMid string `json:"freebase_mid"`
FreebaseID string `json:"freebase_id"`
TvdbID int `json:"tvdb_id"`
TvrageID int `json:"tvrage_id"`
}
type TMDBKeywords struct {
// ID int `json:"id"`
Results []struct {
Name string `json:"name"`
ID int `json:"id"`
} `json:"results"`
}
func getTMDBKey() string {
if Config.TMDB_KEY != "" {
return Config.TMDB_KEY
}
return "d047fa61d926371f277e7a83c9c4ff2c"
}
func tmdbAPIRequest(ep string, p map[string]string) ([]byte, error) {
println("tmdbAPIRequest:", ep)
slog.Debug("tmdbAPIRequest", "endpoint", ep, "params", p)
base, err := url.Parse("https://api.themoviedb.org/3")
if err != nil {
return nil, errors.New("failed to parse api uri")
@@ -226,7 +510,7 @@ func tmdbAPIRequest(ep string, p map[string]string) ([]byte, error) {
// Query params
params := url.Values{}
params.Add("api_key", "d047fa61d926371f277e7a83c9c4ff2c")
params.Add("api_key", getTMDBKey())
params.Add("language", "en-US")
for k, v := range p {
params.Add(k, v)
@@ -246,7 +530,7 @@ func tmdbAPIRequest(ep string, p map[string]string) ([]byte, error) {
return nil, err
}
if res.StatusCode != 200 {
println("TMDB non 200 status code:", res.StatusCode)
slog.Error("TMDB non 200 status code:", "status_code", res.StatusCode)
return nil, errors.New(string(body))
}
return body, nil
+49
View File
@@ -0,0 +1,49 @@
package main
import (
"errors"
"log/slog"
"time"
"gorm.io/gorm"
)
type TokenType string
var (
TOKENTYPE_ADMIN TokenType = "ADMIN"
)
type Token struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time `json:"createdAt"`
Value string `gorm:"not null"`
Type TokenType `gorm:"not null"`
UserID uint `gorm:"not null"`
}
const tokenMaxAge = 2 * time.Minute
func createOneUseToken(db *gorm.DB, t TokenType, userId uint) (string, error) {
token, err := generateString(8)
if err != nil {
slog.Error("createOneUseToken: Failed to generate string!", "error", err)
return "", errors.New("failed to generate token")
}
res := db.Create(&Token{Type: t, Value: token, UserID: userId})
if res.Error != nil {
slog.Error("createOneUseToken: Failed to insert token into db!", "error", res.Error)
return "", errors.New("failed to generate token")
}
return token, nil
}
// Cleans up tokens older than 2m.
func cleanupTokens(db *gorm.DB) {
slog.Debug("cleanupTokens: Cleaning up old tokens from db")
twoMinsAgo := time.Now().Add(-tokenMaxAge)
resp := db.Where("created_at < ?", twoMinsAgo).Delete(&Token{})
if resp.Error != nil {
slog.Error("cleanupTokens: Failed to run DELETE on old tokens!", "error", resp.Error)
}
}
+182
View File
@@ -0,0 +1,182 @@
package main
import (
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"log"
"log/slog"
"path"
"path/filepath"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// Public user details for search results
type PublicUser struct {
ID uint `json:"id"`
Username string `json:"username"`
AvatarID uint `json:"-"`
Avatar Image `json:"avatar"`
Bio string `json:"bio,omitempty"`
}
// Private user details, for returning users details to themselves
type PrivateUser struct {
Username string `json:"username"`
Type UserType `json:"type"`
Permissions int `json:"permissions"`
AvatarID uint `json:"-"`
Avatar Image `json:"avatar"`
Bio string `json:"bio"`
}
type UserBioUpdateRequest struct {
NewBio string `json:"newBio" binding:"max=128"`
}
// Update user settings
func userUpdate(db *gorm.DB, userId uint, ur UserSettings) (UserSettings, error) {
slog.Debug("user update request running", "user_id", userId, "ur", ur)
user := new(User)
res := db.Where("id = ?", userId).Take(&user)
if res.Error != nil {
slog.Error("user update failed", "user_id", userId, "error", res.Error)
return UserSettings{}, errors.New("failed to retrieve user")
}
if ur.HideSpoilers != nil {
user.HideSpoilers = ur.HideSpoilers
}
if ur.Private != nil {
user.Private = ur.Private
}
if ur.PrivateThoughts != nil {
user.PrivateThoughts = ur.PrivateThoughts
}
if ur.IncludePreviouslyWatched != nil {
user.IncludePreviouslyWatched = ur.IncludePreviouslyWatched
}
db.Save(&user)
return UserSettings{
Private: user.Private,
PrivateThoughts: user.PrivateThoughts,
HideSpoilers: user.HideSpoilers,
IncludePreviouslyWatched: user.IncludePreviouslyWatched,
}, nil
}
func userGetSettings(db *gorm.DB, userId uint) (UserSettings, error) {
slog.Debug("user update request running", "user_id", userId)
user := new(User)
res := db.Where("id = ?", userId).Take(&user)
if res.Error != nil {
slog.Error("user get failed", "user_id", userId, "error", res.Error)
return UserSettings{}, errors.New("failed to retrieve user")
}
return UserSettings{
Private: user.Private,
PrivateThoughts: user.PrivateThoughts,
HideSpoilers: user.HideSpoilers,
IncludePreviouslyWatched: user.IncludePreviouslyWatched,
}, nil
}
func userSearch(db *gorm.DB, currentUsersId uint, q string) ([]PublicUser, error) {
slog.Debug("user search request running", "query", q)
users := new([]PublicUser)
res := db.Where("private = 0 AND username LIKE ? AND id != ?", "%"+q+"%", currentUsersId).Table("users").Find(&users)
if res.Error != nil {
slog.Error("user search failed", "error", res.Error)
return []PublicUser{}, errors.New("failed to find users")
}
return *users, nil
}
func getUserInfo(db *gorm.DB, currentUsersId uint) (PrivateUser, error) {
slog.Debug("user get info request running")
user := new(PrivateUser)
res := db.Where("id = ?", currentUsersId).Table("users").Preload("Avatar").Take(&user)
if res.Error != nil {
slog.Error("user get info failed", "error", res.Error)
return PrivateUser{}, errors.New("failed to find current user")
}
return *user, nil
}
// For getting a public user's info, when viewing their list for example
func getUserPublicInfo(db *gorm.DB, userId uint, username string) (PublicUser, error) {
slog.Debug("user get info request running")
user := new(PublicUser)
res := db.Where("private = 0 AND id = ? AND username = ?", userId, username).Table("users").Preload("Avatar").Take(&user)
if res.Error != nil {
slog.Error("public user get info failed", "error", res.Error)
return PublicUser{}, errors.New("failed to find user")
}
return *user, nil
}
func userUpdateBio(db *gorm.DB, userId uint, newBio string) error {
slog.Debug("userUpdateBio request running", "user_id", userId, "newBio", newBio)
if res := db.Model(&User{}).Where("id = ?", userId).Update("bio", newBio); res.Error != nil {
slog.Error("userUpdateBio failed", "user_id", userId, "error", res.Error)
return errors.New("failed to update bio")
}
return nil
}
func uploadUserAvatar(c *gin.Context, db *gorm.DB, userId uint) (Image, error) {
file, err := c.FormFile("avatar")
if err != nil {
slog.Error("failed to get file", "error", err)
return Image{}, errors.New("no file found")
}
slog.Debug("an avatar is being uploaded", "name", file.Filename)
f, _ := file.Open()
if err := isValidImageType(f); err != nil {
return Image{}, errors.New("invalid image type")
}
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
log.Fatal(err) // TODO nu le fatal
}
hs := hex.EncodeToString(h.Sum(nil))
slog.Debug("image hash calculated", "hash", hs, "first_letter", hs[0:1])
// Upload the file to specific dst.
outp := path.Join("img/up/", hs[0:1], hs+filepath.Ext(file.Filename))
c.SaveUploadedFile(file, path.Join(DataPath, outp))
_, err = f.Seek(0, 0)
if err != nil {
slog.Error("uploadUserAvatar seeking back to start of image failed", "error", err)
}
// No need to remove old image, the daily cleanup task will handle removing unused ones.
var img Image
err = db.Transaction(func(tx *gorm.DB) error {
// Insert avatar into db
img, err = insertImage(db, hs, outp, f)
if err != nil {
return err
}
if img.ID == 0 {
return errors.New("image has no id")
}
// Update users avatar to newly inserted
if err := tx.Where("id = ?", userId).Updates(&User{AvatarID: img.ID}).Error; err != nil {
return err
}
// commit transaction if no errors
return nil
})
if err != nil {
slog.Error("uploadUserAvatar failed!", "error", err)
return Image{}, errors.New("uploadUserAvatar transaction failed")
}
return img, nil
}
+140 -21
View File
@@ -1,14 +1,22 @@
package main
import (
"bufio"
"fmt"
"io"
"log"
"log/slog"
"net/http"
"net/http/httputil"
"os"
"os/exec"
"path"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"gopkg.in/natefinch/lumberjack.v2"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
@@ -20,16 +28,28 @@ type GormModel struct {
DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"`
}
var AvailableAuthProviders = []string{}
var (
ServerInSetup = false
logLevel = new(slog.LevelVar)
)
func main() {
fmt.Println("Watcharr Starting")
err := godotenv.Load()
if err != nil {
log.Fatal("Failed to load vars from .env file:", err)
// Do not fail if file does not exist
if !os.IsNotExist(err) {
log.Fatal("Failed to load vars from .env file:", err)
}
}
ensureEnv()
multiw := setupLogging()
slog.Info("Watcharr Starting")
if err = readConfig(); err != nil {
log.Fatal("Failed to read server config!", err)
}
setLoggingLevel()
// Ensure data dir exists
err = ensureDirExists("./data")
@@ -37,18 +57,42 @@ func main() {
log.Fatal("Failed to create data dir:", err)
}
db, err := gorm.Open(sqlite.Open("./data/watcharr.db"), &gorm.Config{})
if err != nil {
panic("failed to connect to database")
// Check if we want to be in DEV or PROD
isProd := true
if os.Getenv("MODE") == "DEV" {
slog.Info("Starting in DEV mode")
isProd = false
}
err = db.AutoMigrate(&User{}, &Content{}, &Watched{})
db, err := gorm.Open(sqlite.Open("./data/watcharr.db"), &gorm.Config{TranslateError: true})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
err = db.AutoMigrate(
&User{},
&UserServices{},
&Content{},
&Watched{},
&WatchedSeason{},
&WatchedEpisode{},
&Activity{},
&Token{},
&Follow{},
&Image{},
&Game{},
)
if err != nil {
log.Fatal("Failed to auto migrate database:", err)
}
gin := gin.Default()
gin.Use(cors.New(cors.Config{
if isProd {
go runUI()
gin.SetMode(gin.ReleaseMode)
}
gin.DefaultWriter = multiw
gine := gin.Default()
gine.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization", "accept", "origin", "Cache-Control", "X-Requested-With"},
@@ -56,23 +100,98 @@ func main() {
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
br := newBaseRouter(db, gin)
if isProd {
// Proxy NoRoute requests to UI server
gine.NoRoute(func(c *gin.Context) {
director := func(req *http.Request) {
req.URL.Scheme = "http"
req.URL.Host = "127.0.0.1:3000"
}
proxy := &httputil.ReverseProxy{Director: director}
proxy.ServeHTTP(c.Writer, c.Request)
})
}
br := newBaseRouter(db, gine.Group("/api"))
// Only add setup routes if there are no users found in db.
var userCount int64
if uresp := db.Model(&User{}).Count(&userCount); uresp.Error == nil {
if userCount != 0 {
slog.Debug("registered users found.. skipped creating setup routes.")
} else {
slog.Info("No users found.. creating setup routes.")
ServerInSetup = true
br.addSetupRoutes()
}
} else {
slog.Error("Failed to check if any users exist.. not registering setup routes", "error", uresp.Error)
}
br.addAuthRoutes()
br.addContentRoutes()
br.addGameRoutes()
br.addWatchedRoutes()
br.rg.Static("/img", "./data/img")
br.addActivityRoutes()
br.addProfileRoutes()
br.addJellyfinRoutes()
br.addPlexRoutes()
br.addUserRoutes()
br.addFollowRoutes()
br.addImportRoutes()
br.addServerRoutes()
br.addFeatureRoutes()
br.addSonarrRoutes()
br.addRadarrRoutes()
br.addJobRoutes()
br.rg.Static("/img", path.Join(DataPath, "img"))
gin.Run("0.0.0.0:3080")
go setupTasks(db)
gine.Run("0.0.0.0:3080")
}
// Ensure all required environment variables are set.
func ensureEnv() {
if os.Getenv("JWT_SECRET") == "" {
log.Fatal("JWT_SECRET env var missing!")
}
// Setup slog defaults
func setupLogging() io.Writer {
// logLevel = new(slog.LevelVar)
multiw := io.MultiWriter(&lumberjack.Logger{
Filename: "./data/watcharr.log",
MaxSize: 1, // megabytes
MaxBackups: 3,
MaxAge: 28, // days
Compress: false,
}, os.Stdout)
slog.SetDefault(slog.New(
slog.NewTextHandler(multiw, &slog.HandlerOptions{Level: logLevel}),
))
return multiw
}
if os.Getenv("JELLYFIN_HOST") != "" {
AvailableAuthProviders = append(AvailableAuthProviders, "jellyfin")
// Set loggin level from config
func setLoggingLevel() {
if Config.DEBUG {
logLevel.Set(slog.LevelDebug)
} else {
logLevel.Set(slog.LevelInfo)
}
slog.Info("Logging level set", "logging_level", logLevel)
}
// Run UI server
func runUI() {
cmd := exec.Command("node", "ui/index.js")
cmdReader, err := cmd.StdoutPipe()
if err != nil {
log.Fatal("UI ERR @ get stdout read pipe: ", err)
}
scanner := bufio.NewScanner(cmdReader)
go func() {
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}()
if err := cmd.Start(); err != nil {
log.Fatal("UI ERR @ command start: ", err)
}
if err := cmd.Wait(); err != nil {
log.Fatal("UI ERR @ command wait: ", err)
}
}

Some files were not shown because too many files have changed in this diff Show More