From d34d87957e4d1cba673475c9aa8081b42d182daa Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 17 Apr 2024 21:59:18 +0200 Subject: [PATCH 01/26] Use msgspec for messages deserialization --- scanner/matcher/__init__.py | 4 ++-- scanner/matcher/subscriber.py | 32 ++++++++++++++++++++------------ scanner/requirements.txt | 2 +- scanner/scanner/requirements.txt | 1 - shell.nix | 1 + 5 files changed, 24 insertions(+), 16 deletions(-) delete mode 100644 scanner/scanner/requirements.txt diff --git a/scanner/matcher/__init__.py b/scanner/matcher/__init__.py index e7ec19da..4445f9aa 100644 --- a/scanner/matcher/__init__.py +++ b/scanner/matcher/__init__.py @@ -14,5 +14,5 @@ async def main(): async with KyooClient() as kyoo, Subscriber() as sub: provider = Provider.get_default(kyoo.client) - scanner = Matcher(kyoo, provider) - await sub.listen(scanner) + matcher = Matcher(kyoo, provider) + await sub.listen(matcher) diff --git a/scanner/matcher/subscriber.py b/scanner/matcher/subscriber.py index 15d25710..b8b7bac9 100644 --- a/scanner/matcher/subscriber.py +++ b/scanner/matcher/subscriber.py @@ -1,7 +1,6 @@ import asyncio -from dataclasses import dataclass -from dataclasses_json import DataClassJsonMixin -from typing import Literal +from typing import Union +from msgspec import Struct, json import os import logging from aio_pika import connect_robust @@ -11,12 +10,19 @@ from matcher.matcher import Matcher logger = logging.getLogger(__name__) +class Message(Struct, tag_field="action", tag=str.lower): + pass -@dataclass -class Message(DataClassJsonMixin): - action: Literal["scan", "delete"] +class Scan(Message): path: str +class Delete(Message): + path: str + +class Identify(Message): + pass + +decoder = json.Decoder(Union[Scan, Delete, Identify]) class Subscriber: QUEUE = "scanner" @@ -36,13 +42,15 @@ class Subscriber: async def listen(self, scanner: Matcher): async def on_message(message: AbstractIncomingMessage): - msg = Message.from_json(message.body) + msg = decoder.decode(message.body) ack = False - match msg.action: - case "scan": - ack = await scanner.identify(msg.path) - case "delete": - ack = await scanner.delete(msg.path) + match msg: + case Scan(path): + ack = await scanner.identify(path) + case Delete(path): + ack = await scanner.delete(path) + # case Identify(): + # ack = await scanner.delete(msg.path) case _: logger.error(f"Invalid action: {msg.action}") if ack: diff --git a/scanner/requirements.txt b/scanner/requirements.txt index cdeb31bd..ca23e9ed 100644 --- a/scanner/requirements.txt +++ b/scanner/requirements.txt @@ -3,4 +3,4 @@ aiohttp jsons watchfiles aio-pika -dataclasses-json +msgspec diff --git a/scanner/scanner/requirements.txt b/scanner/scanner/requirements.txt deleted file mode 100644 index e092edcc..00000000 --- a/scanner/scanner/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -aio-pika diff --git a/shell.nix b/shell.nix index 482414f3..6f92b2eb 100644 --- a/shell.nix +++ b/shell.nix @@ -9,6 +9,7 @@ aio-pika requests dataclasses-json + msgspec ]); dotnet = with pkgs.dotnetCorePackages; combinePackages [ From 8a816b587fb9097cfba876957f5f70aa111246ab Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 17 Apr 2024 23:01:36 +0200 Subject: [PATCH 02/26] Add refresh message handler --- scanner/matcher/matcher.py | 28 ++++++++++++++++++++++++++++ scanner/matcher/subscriber.py | 13 +++++++------ scanner/providers/kyoo_client.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/scanner/matcher/matcher.py b/scanner/matcher/matcher.py index bc3277c8..b0b76932 100644 --- a/scanner/matcher/matcher.py +++ b/scanner/matcher/matcher.py @@ -1,4 +1,5 @@ from datetime import timedelta +from typing import Literal import asyncio from logging import getLogger from providers.provider import Provider, ProviderError @@ -165,3 +166,30 @@ class Matcher: return await self._client.post("seasons", data=season.to_kyoo()) return await create_season(show_id, season_number) + + async def refresh( + self, + kind: Literal["collection", "movie", "episode", "show", "season"], + kyoo_id: str, + ): + identify_table = { + "collection": lambda _, id: self._provider.identify_collection( + id["dataId"] + ), + "movie": lambda _, id: self._provider.identify_movie(id["dataId"]), + "show": lambda _, id: self._provider.identify_show(id["dataId"]), + "season": lambda season, id: self._provider.identify_season( + id["dataId"], season["seasonNumber"] + ), + "episode": lambda episode, id: self._provider.identify_episode( + id["showId"], id["season"], id["episode"], episode["absoluteNumber"] + ), + } + current = await self._client.get(kind, kyoo_id) + if self._provider.name not in current["externalId"]: + logger.error(f"Could not refresh metadata of {kind}/{kyoo_id}. Missisg provider id.") + return False + provider_id = current["externalId"][self._provider.name] + new_value = await identify_table[kind](current, provider_id) + await self._client.put(f"{kind}/{kyoo_id}", data=new_value.to_kyoo()) + return True diff --git a/scanner/matcher/subscriber.py b/scanner/matcher/subscriber.py index b8b7bac9..103b55bd 100644 --- a/scanner/matcher/subscriber.py +++ b/scanner/matcher/subscriber.py @@ -1,5 +1,5 @@ import asyncio -from typing import Union +from typing import Union, Literal from msgspec import Struct, json import os import logging @@ -19,10 +19,11 @@ class Scan(Message): class Delete(Message): path: str -class Identify(Message): - pass +class Refresh(Message): + kind: Literal["collection", "show", "movie", "season", "episode"] + id: str -decoder = json.Decoder(Union[Scan, Delete, Identify]) +decoder = json.Decoder(Union[Scan, Delete, Refresh]) class Subscriber: QUEUE = "scanner" @@ -49,8 +50,8 @@ class Subscriber: ack = await scanner.identify(path) case Delete(path): ack = await scanner.delete(path) - # case Identify(): - # ack = await scanner.delete(msg.path) + case Refresh(kind, id): + ack = await scanner.refresh(kind, id) case _: logger.error(f"Invalid action: {msg.action}") if ack: diff --git a/scanner/providers/kyoo_client.py b/scanner/providers/kyoo_client.py index 5dfbdc7b..a8ef8ccd 100644 --- a/scanner/providers/kyoo_client.py +++ b/scanner/providers/kyoo_client.py @@ -154,3 +154,35 @@ class KyooClient: r.raise_for_status() await self.delete_issue(path) + + async def get( + self, kind: Literal["movie", "show", "season", "episode", "collection"], id: str + ): + async with self.client.get( + f"{self._url}/{kind}/{id}", + headers={"X-API-Key": self._api_key}, + ) as r: + if not r.ok: + logger.error(f"Request error: {await r.text()}") + r.raise_for_status() + return await r.json() + + async def put(self, path: str, *, data: dict[str, Any]): + logger.debug( + "Sending %s: %s", + path, + jsons.dumps( + data, + key_transformer=jsons.KEY_TRANSFORMER_CAMELCASE, + jdkwargs={"indent": 4}, + ), + ) + async with self.client.post( + f"{self._url}/{path}", + json=data, + headers={"X-API-Key": self._api_key}, + ) as r: + # Allow 409 and continue as if it worked. + if not r.ok and r.status != 409: + logger.error(f"Request error: {await r.text()}") + r.raise_for_status() From dc5fd8f5a3c73ef5ea1bd370a92b3d12b2170e24 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 17 Apr 2024 23:57:05 +0200 Subject: [PATCH 03/26] Fix put --- back/src/Kyoo.Core/Views/Helper/CrudApi.cs | 27 ++++++++++++++++++ scanner/matcher/subscriber.py | 32 ++++++++++++---------- scanner/providers/kyoo_client.py | 2 +- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/back/src/Kyoo.Core/Views/Helper/CrudApi.cs b/back/src/Kyoo.Core/Views/Helper/CrudApi.cs index ae389f95..44e4447f 100644 --- a/back/src/Kyoo.Core/Views/Helper/CrudApi.cs +++ b/back/src/Kyoo.Core/Views/Helper/CrudApi.cs @@ -170,6 +170,33 @@ public class CrudApi : BaseApi return await Repository.Edit(resource); } + /// + /// Edit + /// + /// + /// Edit an item. If the ID is specified it will be used to identify the resource. + /// If not, the slug will be used to identify it. + /// + /// The id or slug of the resource. + /// The resource to edit. + /// The edited resource. + /// The resource in the request body is invalid. + /// No item found with the specified ID (or slug). + [HttpPut("{identifier:id}")] + [PartialPermission(Kind.Write)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> Edit(Identifier identifier, [FromBody] T resource) + { + Guid id = await identifier.Match( + id => Task.FromResult(id), + async slug => (await Repository.Get(slug)).Id + ); + resource.Id = id; + return await Repository.Edit(resource); + } + /// /// Patch /// diff --git a/scanner/matcher/subscriber.py b/scanner/matcher/subscriber.py index 103b55bd..728cf5bd 100644 --- a/scanner/matcher/subscriber.py +++ b/scanner/matcher/subscriber.py @@ -43,20 +43,24 @@ class Subscriber: async def listen(self, scanner: Matcher): async def on_message(message: AbstractIncomingMessage): - msg = decoder.decode(message.body) - ack = False - match msg: - case Scan(path): - ack = await scanner.identify(path) - case Delete(path): - ack = await scanner.delete(path) - case Refresh(kind, id): - ack = await scanner.refresh(kind, id) - case _: - logger.error(f"Invalid action: {msg.action}") - if ack: - await message.ack() - else: + try: + msg = decoder.decode(message.body) + ack = False + match msg: + case Scan(path): + ack = await scanner.identify(path) + case Delete(path): + ack = await scanner.delete(path) + case Refresh(kind, id): + ack = await scanner.refresh(kind, id) + case _: + logger.error(f"Invalid action: {msg.action}") + if ack: + await message.ack() + else: + await message.reject() + except Exception as e: + logger.exception("Unhandled error", exc_info=e) await message.reject() # Allow up to 20 scan requests to run in parallel on the same listener. diff --git a/scanner/providers/kyoo_client.py b/scanner/providers/kyoo_client.py index a8ef8ccd..b804b740 100644 --- a/scanner/providers/kyoo_client.py +++ b/scanner/providers/kyoo_client.py @@ -177,7 +177,7 @@ class KyooClient: jdkwargs={"indent": 4}, ), ) - async with self.client.post( + async with self.client.put( f"{self._url}/{path}", json=data, headers={"X-API-Key": self._api_key}, From 69e8340c95ac093a30924c4001a9a16b3906eb34 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 19 Apr 2024 13:51:53 +0200 Subject: [PATCH 04/26] Take images url from api --- .../models/src/resources/collection.ts | 17 +++--- .../models/src/resources/episode.base.ts | 10 ++-- front/packages/models/src/resources/movie.ts | 10 ++-- front/packages/models/src/resources/person.ts | 37 ++++++------ front/packages/models/src/resources/season.ts | 57 +++++++++---------- front/packages/models/src/resources/show.ts | 10 ++-- front/packages/models/src/traits/images.ts | 30 ++-------- 7 files changed, 74 insertions(+), 97 deletions(-) diff --git a/front/packages/models/src/resources/collection.ts b/front/packages/models/src/resources/collection.ts index e761b8dc..aa85e4c9 100644 --- a/front/packages/models/src/resources/collection.ts +++ b/front/packages/models/src/resources/collection.ts @@ -19,10 +19,11 @@ */ import { z } from "zod"; -import { withImages, ResourceP } from "../traits"; +import { ImagesP, ResourceP } from "../traits"; -export const CollectionP = withImages( - ResourceP("collection").extend({ +export const CollectionP = ResourceP("collection") + .merge(ImagesP) + .extend({ /** * The title of this collection. */ @@ -31,11 +32,11 @@ export const CollectionP = withImages( * The summary of this show. */ overview: z.string().nullable(), - }), -).transform((x) => ({ - ...x, - href: `/collection/${x.slug}`, -})); + }) + .transform((x) => ({ + ...x, + href: `/collection/${x.slug}`, + })); /** * A class representing collections of show or movies. diff --git a/front/packages/models/src/resources/episode.base.ts b/front/packages/models/src/resources/episode.base.ts index db2e86d2..4908c773 100644 --- a/front/packages/models/src/resources/episode.base.ts +++ b/front/packages/models/src/resources/episode.base.ts @@ -20,11 +20,12 @@ import { z } from "zod"; import { zdate } from "../utils"; -import { withImages, imageFn } from "../traits"; +import { ImagesP, imageFn } from "../traits"; import { ResourceP } from "../traits/resource"; -export const BaseEpisodeP = withImages( - ResourceP("episode").extend({ +export const BaseEpisodeP = ResourceP("episode") + .merge(ImagesP) + .extend({ /** * The season in witch this episode is in. */ @@ -71,8 +72,7 @@ export const BaseEpisodeP = withImages( * The id of the show containing this episode */ showId: z.string(), - }), -) + }) .transform((x) => ({ ...x, runtime: x.runtime === 0 ? null : x.runtime, diff --git a/front/packages/models/src/resources/movie.ts b/front/packages/models/src/resources/movie.ts index df425d6d..7554df1a 100644 --- a/front/packages/models/src/resources/movie.ts +++ b/front/packages/models/src/resources/movie.ts @@ -20,7 +20,7 @@ import { z } from "zod"; import { zdate } from "../utils"; -import { withImages, ResourceP, imageFn } from "../traits"; +import { ImagesP, ResourceP, imageFn } from "../traits"; import { Genre } from "./genre"; import { StudioP } from "./studio"; import { Status } from "./show"; @@ -28,8 +28,9 @@ import { CollectionP } from "./collection"; import { MetadataP } from "./metadata"; import { WatchStatusP } from "./watch-status"; -export const MovieP = withImages( - ResourceP("movie").extend({ +export const MovieP = ResourceP("movie") + .merge(ImagesP) + .extend({ /** * The title of this movie. */ @@ -104,8 +105,7 @@ export const MovieP = withImages( * Metadata of what an user as started/planned to watch. */ watchStatus: WatchStatusP.optional().nullable(), - }), -) + }) .transform((x) => ({ ...x, runtime: x.runtime === 0 ? null : x.runtime, diff --git a/front/packages/models/src/resources/person.ts b/front/packages/models/src/resources/person.ts index 00994383..f069efa7 100644 --- a/front/packages/models/src/resources/person.ts +++ b/front/packages/models/src/resources/person.ts @@ -19,28 +19,25 @@ */ import { z } from "zod"; -import { withImages } from "../traits"; -import { ResourceP } from "../traits/resource"; +import { ImagesP, ResourceP } from "../traits"; -export const PersonP = withImages( - ResourceP("people").extend({ - /** - * The name of this person. - */ - name: z.string(), - /** - * The type of work the person has done for the show. That can be something like "Actor", - * "Writer", "Music", "Voice Actor"... - */ - type: z.string().optional(), +export const PersonP = ResourceP("people").merge(ImagesP).extend({ + /** + * The name of this person. + */ + name: z.string(), + /** + * The type of work the person has done for the show. That can be something like "Actor", + * "Writer", "Music", "Voice Actor"... + */ + type: z.string().optional(), - /** - * The role the People played. This is mostly used to inform witch character was played for actor - * and voice actors. - */ - role: z.string().optional(), - }), -); + /** + * The role the People played. This is mostly used to inform witch character was played for actor + * and voice actors. + */ + role: z.string().optional(), +}); /** * A studio that make shows. diff --git a/front/packages/models/src/resources/season.ts b/front/packages/models/src/resources/season.ts index 839e1519..9c5c1fa2 100644 --- a/front/packages/models/src/resources/season.ts +++ b/front/packages/models/src/resources/season.ts @@ -20,37 +20,34 @@ import { z } from "zod"; import { zdate } from "../utils"; -import { withImages } from "../traits"; -import { ResourceP } from "../traits/resource"; +import { ImagesP, ResourceP } from "../traits"; -export const SeasonP = withImages( - ResourceP("season").extend({ - /** - * The name of this season. - */ - name: z.string(), - /** - * The number of this season. This can be set to 0 to indicate specials. - */ - seasonNumber: z.number(), - /** - * A quick overview of this season. - */ - overview: z.string().nullable(), - /** - * The starting air date of this season. - */ - startDate: zdate().nullable(), - /** - * The ending date of this season. - */ - endDate: zdate().nullable(), - /** - * The number of episodes available on kyoo of this season. - */ - episodesCount: z.number(), - }), -); +export const SeasonP = ResourceP("season").merge(ImagesP).extend({ + /** + * The name of this season. + */ + name: z.string(), + /** + * The number of this season. This can be set to 0 to indicate specials. + */ + seasonNumber: z.number(), + /** + * A quick overview of this season. + */ + overview: z.string().nullable(), + /** + * The starting air date of this season. + */ + startDate: zdate().nullable(), + /** + * The ending date of this season. + */ + endDate: zdate().nullable(), + /** + * The number of episodes available on kyoo of this season. + */ + episodesCount: z.number(), +}); /** * A season of a Show. diff --git a/front/packages/models/src/resources/show.ts b/front/packages/models/src/resources/show.ts index 9275751a..8b8ee32c 100644 --- a/front/packages/models/src/resources/show.ts +++ b/front/packages/models/src/resources/show.ts @@ -20,7 +20,7 @@ import { z } from "zod"; import { zdate } from "../utils"; -import { withImages, ResourceP } from "../traits"; +import { ImagesP, ResourceP } from "../traits"; import { Genre } from "./genre"; import { StudioP } from "./studio"; import { BaseEpisodeP } from "./episode.base"; @@ -37,8 +37,9 @@ export enum Status { Planned = "Planned", } -export const ShowP = withImages( - ResourceP("show").extend({ +export const ShowP = ResourceP("show") + .merge(ImagesP) + .extend({ /** * The title of this show. */ @@ -103,8 +104,7 @@ export const ShowP = withImages( * The number of episodes in this show. */ episodesCount: z.number().int().gte(0).optional(), - }), -) + }) .transform((x) => { if (!x.thumbnail && x.poster) { x.thumbnail = { ...x.poster }; diff --git a/front/packages/models/src/traits/images.ts b/front/packages/models/src/traits/images.ts index 9142e70d..57627532 100644 --- a/front/packages/models/src/traits/images.ts +++ b/front/packages/models/src/traits/images.ts @@ -19,7 +19,7 @@ */ import { Platform } from "react-native"; -import { ZodObject, ZodRawShape, z } from "zod"; +import { z } from "zod"; import { lastUsedUrl } from ".."; export const imageFn = (url: string) => @@ -28,9 +28,12 @@ export const imageFn = (url: string) => export const Img = z.object({ source: z.string(), blurhash: z.string(), + low: z.string().transform(imageFn), + medium: z.string().transform(imageFn), + high: z.string().transform(imageFn), }); -const ImagesP = z.object({ +export const ImagesP = z.object({ /** * An url to the poster of this resource. If this resource does not have an image, the link will * be null. If the kyoo's instance is not capable of handling this kind of image for the specific @@ -53,28 +56,7 @@ const ImagesP = z.object({ logo: Img.nullable(), }); -const addQualities = (x: object | null | undefined, href: string) => { - if (x === null) return null; - return { - ...x, - low: imageFn(`${href}?quality=low`), - medium: imageFn(`${href}?quality=medium`), - high: imageFn(`${href}?quality=high`), - }; -}; - -export const withImages = (parser: ZodObject) => { - return parser.merge(ImagesP).transform((x) => { - return { - ...x, - poster: addQualities(x.poster, `/${x.kind}/${x.slug}/poster`), - thumbnail: addQualities(x.thumbnail, `/${x.kind}/${x.slug}/thumbnail`), - logo: addQualities(x.logo, `/${x.kind}/${x.slug}/logo`), - }; - }); -}; - /** * Base traits for items that has image resources. */ -export type KyooImage = z.infer & { low: string; medium: string; high: string }; +export type KyooImage = z.infer; From 1d553daeaf3f3c4ebcd07b3282b2a9802a61f2c0 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 19 Apr 2024 21:03:29 +0200 Subject: [PATCH 05/26] Cleanup ef repositories --- .../Repositories/CollectionRepository.cs | 46 +---- .../Repositories/EpisodeRepository.cs | 65 ++++---- .../Repositories/LocalRepository.cs | 157 +++--------------- .../Repositories/MovieRepository.cs | 61 +------ .../Repositories/SeasonRepository.cs | 47 +----- .../Repositories/ShowRepository.cs | 62 +------ .../Repositories/StudioRepository.cs | 40 +---- .../Repositories/UserRepository.cs | 29 +--- .../Kyoo.Core/Extensions/ServiceExtensions.cs | 2 - .../src/Kyoo.Meilisearch/MeilisearchModule.cs | 3 - 10 files changed, 87 insertions(+), 425 deletions(-) diff --git a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs index a217fc85..723c21a6 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs @@ -31,46 +31,20 @@ namespace Kyoo.Core.Controllers; /// /// A local repository to handle collections /// -public class CollectionRepository : LocalRepository +public class CollectionRepository(DatabaseContext database) : LocalRepository(database) { - /// - /// The database handle - /// - private readonly DatabaseContext _database; - - /// - /// Create a new . - /// - /// The database handle to use - /// The thumbnail manager used to store images. - public CollectionRepository(DatabaseContext database, IThumbnailsManager thumbs) - : base(database, thumbs) - { - _database = database; - } - /// public override async Task> Search( string query, Include? include = default ) { - return await AddIncludes(_database.Collections, include) + return await AddIncludes(Database.Collections, include) .Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%")) .Take(20) .ToListAsync(); } - /// - public override async Task Create(Collection obj) - { - await base.Create(obj); - _database.Entry(obj).State = EntityState.Added; - await _database.SaveChangesAsync(() => Get(obj.Slug)); - await IRepository.OnResourceCreated(obj); - return obj; - } - /// protected override async Task Validate(Collection resource) { @@ -82,21 +56,13 @@ public class CollectionRepository : LocalRepository public async Task AddMovie(Guid id, Guid movieId) { - _database.AddLinks(id, movieId); - await _database.SaveChangesAsync(); + Database.AddLinks(id, movieId); + await Database.SaveChangesAsync(); } public async Task AddShow(Guid id, Guid showId) { - _database.AddLinks(id, showId); - await _database.SaveChangesAsync(); - } - - /// - public override async Task Delete(Collection obj) - { - _database.Entry(obj).State = EntityState.Deleted; - await _database.SaveChangesAsync(); - await base.Delete(obj); + Database.AddLinks(id, showId); + await Database.SaveChangesAsync(); } } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs index b89b3204..d6a8c9ef 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs @@ -32,11 +32,8 @@ namespace Kyoo.Core.Controllers; /// /// A local repository to handle episodes. /// -public class EpisodeRepository( - DatabaseContext database, - IRepository shows, - IThumbnailsManager thumbs -) : LocalRepository(database, thumbs) +public class EpisodeRepository(DatabaseContext database, IRepository shows) + : LocalRepository(database) { static EpisodeRepository() { @@ -64,34 +61,18 @@ public class EpisodeRepository( Include? include = default ) { - return await AddIncludes(database.Episodes, include) + return await AddIncludes(Database.Episodes, include) .Where(x => EF.Functions.ILike(x.Name!, $"%{query}%")) .Take(20) .ToListAsync(); } - protected override Task GetDuplicated(Episode item) - { - if (item is { SeasonNumber: not null, EpisodeNumber: not null }) - return database.Episodes.FirstOrDefaultAsync(x => - x.ShowId == item.ShowId - && x.SeasonNumber == item.SeasonNumber - && x.EpisodeNumber == item.EpisodeNumber - ); - return database.Episodes.FirstOrDefaultAsync(x => - x.ShowId == item.ShowId && x.AbsoluteNumber == item.AbsoluteNumber - ); - } - /// public override async Task Create(Episode obj) { + // Set it for the OnResourceCreated event and the return value. obj.ShowSlug = obj.Show?.Slug ?? (await shows.Get(obj.ShowId)).Slug; - await base.Create(obj); - database.Entry(obj).State = EntityState.Added; - await database.SaveChangesAsync(() => GetDuplicated(obj)); - await IRepository.OnResourceCreated(obj); - return obj; + return await base.Create(obj); } /// @@ -111,7 +92,7 @@ public class EpisodeRepository( } if (resource.SeasonId == null && resource.SeasonNumber != null) { - resource.Season = await database.Seasons.FirstOrDefaultAsync(x => + resource.Season = await Database.Seasons.FirstOrDefaultAsync(x => x.ShowId == resource.ShowId && x.SeasonNumber == resource.SeasonNumber ); } @@ -120,14 +101,40 @@ public class EpisodeRepository( /// public override async Task Delete(Episode obj) { - int epCount = await database + int epCount = await Database .Episodes.Where(x => x.ShowId == obj.ShowId) .Take(2) .CountAsync(); - database.Entry(obj).State = EntityState.Deleted; - await database.SaveChangesAsync(); - await base.Delete(obj); if (epCount == 1) await shows.Delete(obj.ShowId); + else + await base.Delete(obj); + } + + /// + public override async Task DeleteAll(Filter filter) + { + ICollection items = await GetAll(filter); + Guid[] ids = items.Select(x => x.Id).ToArray(); + + await Database.Set().Where(x => ids.Contains(x.Id)).ExecuteDeleteAsync(); + foreach (Episode resource in items) + await IRepository.OnResourceDeleted(resource); + + Guid[] showIds = await Database + .Set() + .Where(filter.ToEfLambda()) + .Select(x => x.Show!) + .Where(x => !x.Episodes!.Any()) + .Select(x => x.Id) + .ToArrayAsync(); + + if (!showIds.Any()) + return; + + Filter[] showFilters = showIds + .Select(x => new Filter.Eq(nameof(Show.Id), x)) + .ToArray(); + await shows.DeleteAll(Filter.Or(showFilters)!); } } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs index c0e56b7f..6b5c5026 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs @@ -29,38 +29,14 @@ using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Utils; using Kyoo.Postgresql; -using Kyoo.Utils; using Microsoft.EntityFrameworkCore; namespace Kyoo.Core.Controllers; -/// -/// A base class to create repositories using Entity Framework. -/// -/// The type of this repository -public abstract class LocalRepository : IRepository +public abstract class LocalRepository(DatabaseContext database) : IRepository where T : class, IResource, IQuery { - /// - /// The Entity Framework's Database handle. - /// - protected DbContext Database { get; } - - /// - /// The thumbnail manager used to store images. - /// - private readonly IThumbnailsManager _thumbs; - - /// - /// Create a new base with the given database handle. - /// - /// A database connection to load resources of type - /// The thumbnail manager used to store images. - protected LocalRepository(DbContext database, IThumbnailsManager thumbs) - { - Database = database; - _thumbs = thumbs; - } + public DatabaseContext Database => database; /// public Type RepositoryType => typeof(T); @@ -127,12 +103,6 @@ public abstract class LocalRepository : IRepository return query; } - /// - /// Get a resource from it's ID and make the instance track it. - /// - /// The ID of the resource - /// If the item is not found - /// The tracked resource with the given ID protected virtual async Task GetWithTracking(Guid id) { T? ret = await Database.Set().AsTracking().FirstOrDefaultAsync(x => x.Id == id); @@ -174,11 +144,6 @@ public abstract class LocalRepository : IRepository return ret; } - protected virtual Task GetDuplicated(T item) - { - return GetOrDefault(item.Slug); - } - /// public virtual Task GetOrDefault(Guid id, Include? include = default) { @@ -303,26 +268,9 @@ public abstract class LocalRepository : IRepository public virtual async Task Create(T obj) { await Validate(obj); - if (obj is IThumbnails thumbs) - { - try - { - await _thumbs.DownloadImages(thumbs); - } - catch (DuplicatedItemException e) when (e.Existing is null) - { - throw new DuplicatedItemException(await GetDuplicated(obj)); - } - if (thumbs.Poster != null) - Database.Entry(thumbs).Reference(x => x.Poster).TargetEntry!.State = - EntityState.Added; - if (thumbs.Thumbnail != null) - Database.Entry(thumbs).Reference(x => x.Thumbnail).TargetEntry!.State = - EntityState.Added; - if (thumbs.Logo != null) - Database.Entry(thumbs).Reference(x => x.Logo).TargetEntry!.State = - EntityState.Added; - } + Database.Entry(obj).State = EntityState.Added; + await Database.SaveChangesAsync(() => Get(obj.Slug)); + await IRepository.OnResourceCreated(obj); return obj; } @@ -346,27 +294,11 @@ public abstract class LocalRepository : IRepository /// public virtual async Task Edit(T edited) { - bool lazyLoading = Database.ChangeTracker.LazyLoadingEnabled; - Database.ChangeTracker.LazyLoadingEnabled = false; - try - { - T old = await GetWithTracking(edited.Id); - - Merger.Complete( - old, - edited, - x => x.GetCustomAttribute() == null - ); - await EditRelations(old, edited); - await Database.SaveChangesAsync(); - await IRepository.OnResourceEdited(old); - return old; - } - finally - { - Database.ChangeTracker.LazyLoadingEnabled = lazyLoading; - Database.ChangeTracker.Clear(); - } + await Validate(edited); + Database.Entry(edited).State = EntityState.Modified; + await Database.SaveChangesAsync(); + await IRepository.OnResourceEdited(edited); + return edited; } /// @@ -391,39 +323,9 @@ public abstract class LocalRepository : IRepository } } - /// - /// An overridable method to edit relation of a resource. - /// - /// - /// The non edited resource - /// - /// - /// The new version of . - /// This item will be saved on the database and replace - /// - /// A representing the asynchronous operation. - protected virtual Task EditRelations(T resource, T changed) - { - if (resource is IThumbnails thumbs && changed is IThumbnails chng) - { - Database.Entry(thumbs).Reference(x => x.Poster).IsModified = - thumbs.Poster != chng.Poster; - Database.Entry(thumbs).Reference(x => x.Thumbnail).IsModified = - thumbs.Thumbnail != chng.Thumbnail; - Database.Entry(thumbs).Reference(x => x.Logo).IsModified = thumbs.Logo != chng.Logo; - } - return Validate(resource); - } - - /// - /// A method called just before saving a new resource to the database. - /// It is also called on the default implementation of - /// - /// The resource that will be saved /// /// You can throw this if the resource is illegal and should not be saved. /// - /// A representing the asynchronous operation. protected virtual Task Validate(T resource) { if ( @@ -433,25 +335,8 @@ public abstract class LocalRepository : IRepository return Task.CompletedTask; if (string.IsNullOrEmpty(resource.Slug)) throw new ArgumentException("Resource can't have null as a slug."); - if (int.TryParse(resource.Slug, out int _) || resource.Slug == "random") - { - try - { - MethodInfo? setter = typeof(T).GetProperty(nameof(resource.Slug))!.GetSetMethod(); - if (setter != null) - setter.Invoke(resource, new object[] { resource.Slug + '!' }); - else - throw new ArgumentException( - "Resources slug can't be number only or the literal \"random\"." - ); - } - catch - { - throw new ArgumentException( - "Resources slug can't be number only or the literal \"random\"." - ); - } - } + if (resource.Slug == "random") + throw new ArgumentException("Resources slug can't be the literal \"random\"."); return Task.CompletedTask; } @@ -470,18 +355,20 @@ public abstract class LocalRepository : IRepository } /// - public virtual Task Delete(T obj) + public virtual async Task Delete(T obj) { - IRepository.OnResourceDeleted(obj); - if (obj is IThumbnails thumbs) - return _thumbs.DeleteImages(thumbs); - return Task.CompletedTask; + await Database.Set().Where(x => x.Id == obj.Id).ExecuteDeleteAsync(); + await IRepository.OnResourceDeleted(obj); } /// - public async Task DeleteAll(Filter filter) + public virtual async Task DeleteAll(Filter filter) { - foreach (T resource in await GetAll(filter)) - await Delete(resource); + ICollection items = await GetAll(filter); + Guid[] ids = items.Select(x => x.Id).ToArray(); + await Database.Set().Where(x => ids.Contains(x.Id)).ExecuteDeleteAsync(); + + foreach (T resource in items) + await IRepository.OnResourceDeleted(resource); } } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs index 602f8e8a..d9821252 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs @@ -27,82 +27,29 @@ using Microsoft.EntityFrameworkCore; namespace Kyoo.Core.Controllers; -/// -/// A local repository to handle shows -/// -public class MovieRepository : LocalRepository +public class MovieRepository(DatabaseContext database, IRepository studios) + : LocalRepository(database) { - /// - /// The database handle - /// - private readonly DatabaseContext _database; - - /// - /// A studio repository to handle creation/validation of related studios. - /// - private readonly IRepository _studios; - - public MovieRepository( - DatabaseContext database, - IRepository studios, - IThumbnailsManager thumbs - ) - : base(database, thumbs) - { - _database = database; - _studios = studios; - } - /// public override async Task> Search( string query, Include? include = default ) { - return await AddIncludes(_database.Movies, include) + return await AddIncludes(Database.Movies, include) .Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%")) .Take(20) .ToListAsync(); } - /// - public override async Task Create(Movie obj) - { - await base.Create(obj); - _database.Entry(obj).State = EntityState.Added; - await _database.SaveChangesAsync(() => Get(obj.Slug)); - await IRepository.OnResourceCreated(obj); - return obj; - } - /// protected override async Task Validate(Movie resource) { await base.Validate(resource); if (resource.Studio != null) { - resource.Studio = await _studios.CreateIfNotExists(resource.Studio); + resource.Studio = await studios.CreateIfNotExists(resource.Studio); resource.StudioId = resource.Studio.Id; } } - - /// - protected override async Task EditRelations(Movie resource, Movie changed) - { - await Validate(changed); - - if (changed.Studio != null || changed.StudioId == null) - { - await Database.Entry(resource).Reference(x => x.Studio).LoadAsync(); - resource.Studio = changed.Studio; - } - } - - /// - public override async Task Delete(Movie obj) - { - _database.Remove(obj); - await _database.SaveChangesAsync(); - await base.Delete(obj); - } } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs index 18f53e96..5a31a21a 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs @@ -31,16 +31,8 @@ using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Core.Controllers; -/// -/// A local repository to handle seasons. -/// -public class SeasonRepository : LocalRepository +public class SeasonRepository(DatabaseContext database) : LocalRepository(database) { - /// - /// The database handle - /// - private readonly DatabaseContext _database; - static SeasonRepository() { // Edit seasons slugs when the show's slug changes. @@ -61,31 +53,13 @@ public class SeasonRepository : LocalRepository }; } - /// - /// Create a new . - /// - /// The database handle that will be used - /// The thumbnail manager used to store images. - public SeasonRepository(DatabaseContext database, IThumbnailsManager thumbs) - : base(database, thumbs) - { - _database = database; - } - - protected override Task GetDuplicated(Season item) - { - return _database.Seasons.FirstOrDefaultAsync(x => - x.ShowId == item.ShowId && x.SeasonNumber == item.SeasonNumber - ); - } - /// public override async Task> Search( string query, Include? include = default ) { - return await AddIncludes(_database.Seasons, include) + return await AddIncludes(Database.Seasons, include) .Where(x => EF.Functions.ILike(x.Name!, $"%{query}%")) .Take(20) .ToListAsync(); @@ -94,14 +68,11 @@ public class SeasonRepository : LocalRepository /// public override async Task Create(Season obj) { - await base.Create(obj); + // Set it for the OnResourceCreated event and the return value. obj.ShowSlug = - (await _database.Shows.FirstOrDefaultAsync(x => x.Id == obj.ShowId))?.Slug + (await Database.Shows.FirstOrDefaultAsync(x => x.Id == obj.ShowId))?.Slug ?? throw new ItemNotFoundException($"No show found with ID {obj.ShowId}"); - _database.Entry(obj).State = EntityState.Added; - await _database.SaveChangesAsync(() => GetDuplicated(obj)); - await IRepository.OnResourceCreated(obj); - return obj; + return await base.Create(obj); } /// @@ -120,12 +91,4 @@ public class SeasonRepository : LocalRepository resource.ShowId = resource.Show.Id; } } - - /// - public override async Task Delete(Season obj) - { - _database.Remove(obj); - await _database.SaveChangesAsync(); - await base.Delete(obj); - } } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs index 2253da0f..79f826aa 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs @@ -23,87 +23,33 @@ using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Utils; using Kyoo.Postgresql; -using Kyoo.Utils; using Microsoft.EntityFrameworkCore; namespace Kyoo.Core.Controllers; -/// -/// A local repository to handle shows -/// -public class ShowRepository : LocalRepository +public class ShowRepository(DatabaseContext database, IRepository studios) + : LocalRepository(database) { - /// - /// The database handle - /// - private readonly DatabaseContext _database; - - /// - /// A studio repository to handle creation/validation of related studios. - /// - private readonly IRepository _studios; - - public ShowRepository( - DatabaseContext database, - IRepository studios, - IThumbnailsManager thumbs - ) - : base(database, thumbs) - { - _database = database; - _studios = studios; - } - /// public override async Task> Search( string query, Include? include = default ) { - return await AddIncludes(_database.Shows, include) + return await AddIncludes(Database.Shows, include) .Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%")) .Take(20) .ToListAsync(); } - /// - public override async Task Create(Show obj) - { - await base.Create(obj); - _database.Entry(obj).State = EntityState.Added; - await _database.SaveChangesAsync(() => Get(obj.Slug)); - await IRepository.OnResourceCreated(obj); - return obj; - } - /// protected override async Task Validate(Show resource) { await base.Validate(resource); if (resource.Studio != null) { - resource.Studio = await _studios.CreateIfNotExists(resource.Studio); + resource.Studio = await studios.CreateIfNotExists(resource.Studio); resource.StudioId = resource.Studio.Id; } } - - /// - protected override async Task EditRelations(Show resource, Show changed) - { - await Validate(changed); - - if (changed.Studio != null || changed.StudioId == null) - { - await Database.Entry(resource).Reference(x => x.Studio).LoadAsync(); - resource.Studio = changed.Studio; - } - } - - /// - public override async Task Delete(Show obj) - { - _database.Remove(obj); - await _database.SaveChangesAsync(); - await base.Delete(obj); - } } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs index 7cdc1358..250a5a74 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs @@ -19,11 +19,9 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Utils; using Kyoo.Postgresql; -using Kyoo.Utils; using Microsoft.EntityFrameworkCore; namespace Kyoo.Core.Controllers; @@ -31,51 +29,17 @@ namespace Kyoo.Core.Controllers; /// /// A local repository to handle studios /// -public class StudioRepository : LocalRepository +public class StudioRepository(DatabaseContext database) : LocalRepository(database) { - /// - /// The database handle - /// - private readonly DatabaseContext _database; - - /// - /// Create a new . - /// - /// The database handle - /// The thumbnail manager used to store images. - public StudioRepository(DatabaseContext database, IThumbnailsManager thumbs) - : base(database, thumbs) - { - _database = database; - } - /// public override async Task> Search( string query, Include? include = default ) { - return await AddIncludes(_database.Studios, include) + return await AddIncludes(Database.Studios, include) .Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) .Take(20) .ToListAsync(); } - - /// - public override async Task Create(Studio obj) - { - await base.Create(obj); - _database.Entry(obj).State = EntityState.Added; - await _database.SaveChangesAsync(() => Get(obj.Slug)); - await IRepository.OnResourceCreated(obj); - return obj; - } - - /// - public override async Task Delete(Studio obj) - { - _database.Entry(obj).State = EntityState.Deleted; - await _database.SaveChangesAsync(); - await base.Delete(obj); - } } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs index e4a62db9..81a2c188 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs @@ -40,9 +40,8 @@ public class UserRepository( DatabaseContext database, DbConnection db, SqlVariableContext context, - IThumbnailsManager thumbs, PermissionOption options -) : LocalRepository(database, thumbs), IUserRepository +) : LocalRepository(database), IUserRepository { /// public override async Task> Search( @@ -50,7 +49,7 @@ public class UserRepository( Include? include = default ) { - return await AddIncludes(database.Users, include) + return await AddIncludes(Database.Users, include) .Where(x => EF.Functions.ILike(x.Username, $"%{query}%")) .Take(20) .ToListAsync(); @@ -60,26 +59,14 @@ public class UserRepository( public override async Task Create(User obj) { // If no users exists, the new one will be an admin. Give it every permissions. - if (!await database.Users.AnyAsync()) + if (!await Database.Users.AnyAsync()) obj.Permissions = PermissionOption.Admin; else if (!options.RequireVerification) obj.Permissions = options.NewUser; else obj.Permissions = Array.Empty(); - await base.Create(obj); - database.Entry(obj).State = EntityState.Added; - await database.SaveChangesAsync(() => Get(obj.Slug)); - await IRepository.OnResourceCreated(obj); - return obj; - } - - /// - public override async Task Delete(User obj) - { - database.Entry(obj).State = EntityState.Deleted; - await database.SaveChangesAsync(); - await base.Delete(obj); + return await base.Create(obj); } public Task GetByExternalId(string provider, string id) @@ -109,8 +96,8 @@ public class UserRepository( User user = await GetWithTracking(userId); user.ExternalId[provider] = token; // without that, the change tracker does not find the modification. /shrug - database.Entry(user).Property(x => x.ExternalId).IsModified = true; - await database.SaveChangesAsync(); + Database.Entry(user).Property(x => x.ExternalId).IsModified = true; + await Database.SaveChangesAsync(); return user; } @@ -119,8 +106,8 @@ public class UserRepository( User user = await GetWithTracking(userId); user.ExternalId.Remove(provider); // without that, the change tracker does not find the modification. /shrug - database.Entry(user).Property(x => x.ExternalId).IsModified = true; - await database.SaveChangesAsync(); + Database.Entry(user).Property(x => x.ExternalId).IsModified = true; + await Database.SaveChangesAsync(); return user; } } diff --git a/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs b/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs index 2c92fb56..9bed18bf 100644 --- a/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs +++ b/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs @@ -16,9 +16,7 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System; using System.Linq; -using System.Linq.Expressions; using System.Text.Json; using System.Text.Json.Serialization; using AspNetCore.Proxy; diff --git a/back/src/Kyoo.Meilisearch/MeilisearchModule.cs b/back/src/Kyoo.Meilisearch/MeilisearchModule.cs index bc09420e..f2e971b4 100644 --- a/back/src/Kyoo.Meilisearch/MeilisearchModule.cs +++ b/back/src/Kyoo.Meilisearch/MeilisearchModule.cs @@ -16,9 +16,6 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Meilisearch; From 5b4dc1e9b0edffa02a5bc72dec11400a0be60e24 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 19 Apr 2024 21:10:06 +0200 Subject: [PATCH 06/26] Rename local repository to GenericRepository --- .../Kyoo.Core/Controllers/Repositories/CollectionRepository.cs | 2 +- .../src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs | 2 +- .../Repositories/{LocalRepository.cs => GenericRepository.cs} | 2 +- back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs | 2 +- back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs | 2 +- back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs | 2 +- back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs | 2 +- back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) rename back/src/Kyoo.Core/Controllers/Repositories/{LocalRepository.cs => GenericRepository.cs} (99%) diff --git a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs index 723c21a6..4a3ee1f3 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs @@ -31,7 +31,7 @@ namespace Kyoo.Core.Controllers; /// /// A local repository to handle collections /// -public class CollectionRepository(DatabaseContext database) : LocalRepository(database) +public class CollectionRepository(DatabaseContext database) : GenericRepository(database) { /// public override async Task> Search( diff --git a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs index d6a8c9ef..ff523866 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs @@ -33,7 +33,7 @@ namespace Kyoo.Core.Controllers; /// A local repository to handle episodes. /// public class EpisodeRepository(DatabaseContext database, IRepository shows) - : LocalRepository(database) + : GenericRepository(database) { static EpisodeRepository() { diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/GenericRepository.cs similarity index 99% rename from back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs rename to back/src/Kyoo.Core/Controllers/Repositories/GenericRepository.cs index 6b5c5026..a862aabd 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/GenericRepository.cs @@ -33,7 +33,7 @@ using Microsoft.EntityFrameworkCore; namespace Kyoo.Core.Controllers; -public abstract class LocalRepository(DatabaseContext database) : IRepository +public abstract class GenericRepository(DatabaseContext database) : IRepository where T : class, IResource, IQuery { public DatabaseContext Database => database; diff --git a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs index d9821252..569e6274 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs @@ -28,7 +28,7 @@ using Microsoft.EntityFrameworkCore; namespace Kyoo.Core.Controllers; public class MovieRepository(DatabaseContext database, IRepository studios) - : LocalRepository(database) + : GenericRepository(database) { /// public override async Task> Search( diff --git a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs index 5a31a21a..18d9d47c 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs @@ -31,7 +31,7 @@ using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Core.Controllers; -public class SeasonRepository(DatabaseContext database) : LocalRepository(database) +public class SeasonRepository(DatabaseContext database) : GenericRepository(database) { static SeasonRepository() { diff --git a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs index 79f826aa..9a256453 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs @@ -28,7 +28,7 @@ using Microsoft.EntityFrameworkCore; namespace Kyoo.Core.Controllers; public class ShowRepository(DatabaseContext database, IRepository studios) - : LocalRepository(database) + : GenericRepository(database) { /// public override async Task> Search( diff --git a/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs index 250a5a74..91aba67f 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/StudioRepository.cs @@ -29,7 +29,7 @@ namespace Kyoo.Core.Controllers; /// /// A local repository to handle studios /// -public class StudioRepository(DatabaseContext database) : LocalRepository(database) +public class StudioRepository(DatabaseContext database) : GenericRepository(database) { /// public override async Task> Search( diff --git a/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs index 81a2c188..c98d8eb0 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/UserRepository.cs @@ -41,7 +41,7 @@ public class UserRepository( DbConnection db, SqlVariableContext context, PermissionOption options -) : LocalRepository(database), IUserRepository +) : GenericRepository(database), IUserRepository { /// public override async Task> Search( From e1c04bef5125e7db3e55ab8b5b5e37ceb4b7d1bc Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 19 Apr 2024 21:10:47 +0200 Subject: [PATCH 07/26] Use json for images instead of multiples columns --- .../Resources/Interfaces/IThumbnails.cs | 27 +++++++++++--- .../Utility/JsonKindResolver.cs | 1 - .../Controllers/Repositories/DapperHelper.cs | 35 ++----------------- .../Repositories/DapperRepository.cs | 2 +- .../Repositories/LibraryItemRepository.cs | 2 +- .../Repositories/NewsRepository.cs | 2 +- .../Repositories/WatchStatusRepository.cs | 2 +- back/src/Kyoo.Postgresql/DatabaseContext.cs | 6 ++-- back/src/Kyoo.Postgresql/PostgresContext.cs | 1 + 9 files changed, 34 insertions(+), 44 deletions(-) diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs index 5095dfe4..2bbffa72 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs @@ -17,12 +17,9 @@ // along with Kyoo. If not, see . using System; -using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; -using Kyoo.Abstractions.Models.Attributes; namespace Kyoo.Abstractions.Models; @@ -49,9 +46,13 @@ public interface IThumbnails } [JsonConverter(typeof(ImageConvertor))] -[SqlFirstColumn(nameof(Source))] public class Image { + /// + /// A unique identifier for the image. Used for proper http caches. + /// + public Guid Id { get; set; } + /// /// The original image from another server. /// @@ -63,6 +64,21 @@ public class Image [MaxLength(32)] public string Blurhash { get; set; } + /// + /// The url to access the image in low quality. + /// + public string Low => $"/thumbnails/{Id}?quality=low"; + + /// + /// The url to access the image in medium quality. + /// + public string Medium => $"/thumbnails/{Id}?quality=medium"; + + /// + /// The url to access the image in high quality. + /// + public string High => $"/thumbnails/{Id}?quality=high"; + public Image() { } [JsonConstructor] @@ -97,6 +113,9 @@ public class Image writer.WriteStartObject(); writer.WriteString("source", value.Source); writer.WriteString("blurhash", value.Blurhash); + writer.WriteString("low", value.Low); + writer.WriteString("medium", value.Medium); + writer.WriteString("high", value.High); writer.WriteEndObject(); } } diff --git a/back/src/Kyoo.Abstractions/Utility/JsonKindResolver.cs b/back/src/Kyoo.Abstractions/Utility/JsonKindResolver.cs index 45cdfdaa..db72f7e2 100644 --- a/back/src/Kyoo.Abstractions/Utility/JsonKindResolver.cs +++ b/back/src/Kyoo.Abstractions/Utility/JsonKindResolver.cs @@ -25,7 +25,6 @@ using System.Text.Json; using System.Text.Json.Serialization.Metadata; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Attributes; -using Microsoft.AspNetCore.Http; using static System.Text.Json.JsonNamingPolicy; namespace Kyoo.Utils; diff --git a/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs b/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs index f98e84ce..19383169 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs @@ -252,7 +252,7 @@ public static class DapperHelper this IDbConnection db, FormattableString command, Dictionary config, - Func, T> mapper, + Func, T> mapper, Func> get, SqlVariableContext context, Include? include, @@ -327,23 +327,6 @@ public static class DapperHelper ? ExpendProjections(typeV, prefix, include) : null; - if (typeV.IsAssignableTo(typeof(IThumbnails))) - { - string posterProj = string.Join( - ", ", - new[] { "poster", "thumbnail", "logo" }.Select(x => - $"{prefix}{x}_source as source, {prefix}{x}_blurhash as blurhash" - ) - ); - projection = string.IsNullOrEmpty(projection) - ? posterProj - : $"{projection}, {posterProj}"; - types.InsertRange( - types.IndexOf(typeV) + 1, - Enumerable.Repeat(typeof(Image), 3) - ); - } - if (string.IsNullOrEmpty(projection)) return leadingComa; return $", {projection}{leadingComa}"; @@ -355,19 +338,7 @@ public static class DapperHelper types.ToArray(), items => { - List nItems = new(items.Length); - for (int i = 0; i < items.Length; i++) - { - if (types[i] == typeof(Image)) - continue; - nItems.Add(items[i]); - if (items[i] is not IThumbnails thumbs) - continue; - thumbs.Poster = items[++i] as Image; - thumbs.Thumbnail = items[++i] as Image; - thumbs.Logo = items[++i] as Image; - } - return mapIncludes(mapper(nItems), nItems.Skip(config.Count)); + return mapIncludes(mapper(items), items.Skip(config.Count)); }, ParametersDictionary.LoadFrom(cmd), splitOn: string.Join( @@ -384,7 +355,7 @@ public static class DapperHelper this IDbConnection db, FormattableString command, Dictionary config, - Func, T> mapper, + Func, T> mapper, SqlVariableContext context, Include? include, Filter? filter, diff --git a/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs index 18f0677b..7c37d79d 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs @@ -37,7 +37,7 @@ public abstract class DapperRepository : IRepository protected abstract Dictionary Config { get; } - protected abstract T Mapper(List items); + protected abstract T Mapper(IList items); protected DbConnection Database { get; init; } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs index 958f12ca..9da7cc99 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs @@ -67,7 +67,7 @@ public class LibraryItemRepository : DapperRepository { "c", typeof(Collection) } }; - protected override ILibraryItem Mapper(List items) + protected override ILibraryItem Mapper(IList items) { if (items[0] is Show show && show.Id != Guid.Empty) return show; diff --git a/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs index f55cb31a..c91c2eed 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs @@ -49,7 +49,7 @@ public class NewsRepository : DapperRepository protected override Dictionary Config => new() { { "e", typeof(Episode) }, { "m", typeof(Movie) }, }; - protected override INews Mapper(List items) + protected override INews Mapper(IList items) { if (items[0] is Episode episode && episode.Id != Guid.Empty) return episode; diff --git a/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs index e3137616..3159cbfb 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/WatchStatusRepository.cs @@ -135,7 +135,7 @@ public class WatchStatusRepository( { "_mw", typeof(MovieWatchStatus) }, }; - protected IWatchlist Mapper(List items) + protected IWatchlist Mapper(IList items) { if (items[0] is Show show && show.Id != Guid.Empty) { diff --git a/back/src/Kyoo.Postgresql/DatabaseContext.cs b/back/src/Kyoo.Postgresql/DatabaseContext.cs index 31e7b40f..320021d3 100644 --- a/back/src/Kyoo.Postgresql/DatabaseContext.cs +++ b/back/src/Kyoo.Postgresql/DatabaseContext.cs @@ -201,9 +201,9 @@ public abstract class DatabaseContext : DbContext private static void _HasImages(ModelBuilder modelBuilder) where T : class, IThumbnails { - modelBuilder.Entity().OwnsOne(x => x.Poster); - modelBuilder.Entity().OwnsOne(x => x.Thumbnail); - modelBuilder.Entity().OwnsOne(x => x.Logo); + modelBuilder.Entity().OwnsOne(x => x.Poster, x => x.ToJson()); + modelBuilder.Entity().OwnsOne(x => x.Thumbnail, x => x.ToJson()); + modelBuilder.Entity().OwnsOne(x => x.Logo, x => x.ToJson()); } private static void _HasAddedDate(ModelBuilder modelBuilder) diff --git a/back/src/Kyoo.Postgresql/PostgresContext.cs b/back/src/Kyoo.Postgresql/PostgresContext.cs index f907accd..8052fdcc 100644 --- a/back/src/Kyoo.Postgresql/PostgresContext.cs +++ b/back/src/Kyoo.Postgresql/PostgresContext.cs @@ -94,6 +94,7 @@ public class PostgresContext(DbContextOptions options, IHttpContextAccessor acce typeof(Dictionary), new JsonTypeHandler>() ); + SqlMapper.AddTypeHandler(typeof(Image), new JsonTypeHandler()); SqlMapper.AddTypeHandler(typeof(List), new ListTypeHandler()); SqlMapper.AddTypeHandler(typeof(List), new ListTypeHandler()); SqlMapper.AddTypeHandler(typeof(Wrapper), new Wrapper.Handler()); From d46e6eda644d7413fcf8f5487e3d194acf491dff Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 20 Apr 2024 13:11:23 +0200 Subject: [PATCH 08/26] Rework thumbnail manager to use images id --- .../Controllers/IThumbnailsManager.cs | 41 +-------- .../Repositories/CollectionRepository.cs | 1 - .../Controllers/ThumbnailsManager.cs | 86 +++---------------- .../Views/{Watch => Content}/ProxyApi.cs | 0 .../Kyoo.Core/Views/Content/ThumbnailsApi.cs | 59 +++++++++++++ .../Kyoo.Core/Views/Helper/CrudThumbsApi.cs | 36 ++++---- back/src/Kyoo.Core/Views/Helper/Transcoder.cs | 3 +- .../Views/Resources/CollectionApi.cs | 61 +++++-------- .../Kyoo.Core/Views/Resources/EpisodeApi.cs | 4 +- .../Views/Resources/LibraryItemApi.cs | 22 +---- .../src/Kyoo.Core/Views/Resources/MovieApi.cs | 4 +- back/src/Kyoo.Core/Views/Resources/NewsApi.cs | 6 +- .../Kyoo.Core/Views/Resources/SeasonApi.cs | 27 ++---- back/src/Kyoo.Core/Views/Resources/ShowApi.cs | 46 +++------- 14 files changed, 139 insertions(+), 257 deletions(-) rename back/src/Kyoo.Core/Views/{Watch => Content}/ProxyApi.cs (100%) create mode 100644 back/src/Kyoo.Core/Views/Content/ThumbnailsApi.cs diff --git a/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs b/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs index 715fbedc..f6da3f3b 100644 --- a/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs +++ b/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs @@ -23,56 +23,17 @@ using Kyoo.Abstractions.Models; namespace Kyoo.Abstractions.Controllers; -/// -/// Download images and retrieve the path of those images for a resource. -/// public interface IThumbnailsManager { - /// - /// Download images of a specified item. - /// If no images is available to download, do nothing and silently return. - /// - /// - /// The item to cache images. - /// - /// The type of the item - /// A representing the asynchronous operation. Task DownloadImages(T item) where T : IThumbnails; - /// - /// Retrieve the local path of an image of the given item. - /// - /// The item to retrieve the poster from. - /// The ID of the image. - /// The quality of the image - /// The type of the item - /// The path of the image for the given resource or null if it does not exists. - string GetImagePath(T item, string image, ImageQuality quality) - where T : IThumbnails; + string GetImagePath(Guid imageId, ImageQuality quality); - /// - /// Delete images associated with the item. - /// - /// - /// The item with cached images. - /// - /// The type of the item - /// A representing the asynchronous operation. Task DeleteImages(T item) where T : IThumbnails; - /// - /// Set the user's profile picture - /// - /// The id of the user. - /// The byte stream of the image. Null if no image exist. Task GetUserImage(Guid userId); - /// - /// Set the user's profile picture - /// - /// The id of the user. - /// The byte stream of the image. Null to delete the image. Task SetUserImage(Guid userId, Stream? image); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs index 4a3ee1f3..9a7682ef 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs @@ -20,7 +20,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Utils; using Kyoo.Postgresql; diff --git a/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs b/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs index c9ba209a..0ec1d154 100644 --- a/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs +++ b/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs @@ -42,8 +42,6 @@ public class ThumbnailsManager( Lazy> users ) : IThumbnailsManager { - private static readonly Dictionary> _downloading = []; - private static async Task _WriteTo(SKBitmap bitmap, string path, int quality) { SKData data = bitmap.Encode(SKEncodedImageFormat.Webp, quality); @@ -52,12 +50,16 @@ public class ThumbnailsManager( await reader.CopyToAsync(file); } - private async Task _DownloadImage(Image? image, string localPath, string what) + private async Task _DownloadImage(Image? image, string what) { if (image == null) return; try { + if (image.Id == Guid.Empty) + image.Id = new Guid(); + string localPath = $"/metadata/{image.Id}"; + logger.LogInformation("Downloading image {What}", what); HttpClient client = clientFactory.CreateClient(); @@ -119,86 +121,24 @@ public class ThumbnailsManager( { string name = item is IResource res ? res.Slug : "???"; - string posterPath = - $"{_GetBaseImagePath(item, "poster")}.{ImageQuality.High.ToString().ToLowerInvariant()}.webp"; - bool duplicated = false; - TaskCompletionSource? sync = null; - try - { - lock (_downloading) - { - if (_downloading.ContainsKey(posterPath)) - { - duplicated = true; - sync = _downloading.GetValueOrDefault(posterPath); - } - else - { - sync = new(); - _downloading.Add(posterPath, sync); - } - } - if (duplicated) - { - object? dup = sync != null ? await sync.Task : null; - if (dup != null) - throw new DuplicatedItemException(dup); - } - - await _DownloadImage( - item.Poster, - _GetBaseImagePath(item, "poster"), - $"The poster of {name}" - ); - await _DownloadImage( - item.Thumbnail, - _GetBaseImagePath(item, "thumbnail"), - $"The poster of {name}" - ); - await _DownloadImage( - item.Logo, - _GetBaseImagePath(item, "logo"), - $"The poster of {name}" - ); - } - finally - { - if (!duplicated) - { - lock (_downloading) - { - _downloading.Remove(posterPath); - sync!.SetResult(item); - } - } - } - } - - private static string _GetBaseImagePath(T item, string image) - { - string directory = item switch - { - IResource res - => Path.Combine("/metadata", item.GetType().Name.ToLowerInvariant(), res.Slug), - _ => Path.Combine("/metadata", typeof(T).Name.ToLowerInvariant()) - }; - Directory.CreateDirectory(directory); - return Path.Combine(directory, image); + await _DownloadImage(item.Poster, $"The poster of {name}"); + await _DownloadImage(item.Thumbnail, $"The thumbnail of {name}"); + await _DownloadImage(item.Logo, $"The logo of {name}"); } /// - public string GetImagePath(T item, string image, ImageQuality quality) - where T : IThumbnails + public string GetImagePath(Guid imageId, ImageQuality quality) { - return $"{_GetBaseImagePath(item, image)}.{quality.ToString().ToLowerInvariant()}.webp"; + return $"/metadata/{imageId}.{quality.ToString().ToLowerInvariant()}.webp"; } /// public Task DeleteImages(T item) where T : IThumbnails { - IEnumerable images = new[] { "poster", "thumbnail", "logo" } - .SelectMany(x => _GetBaseImagePath(item, x)) + IEnumerable images = new[] {item.Poster?.Id, item.Thumbnail?.Id, item.Logo?.Id} + .Where(x => x is not null) + .SelectMany(x => $"/metadata/{x}") .SelectMany(x => new[] { diff --git a/back/src/Kyoo.Core/Views/Watch/ProxyApi.cs b/back/src/Kyoo.Core/Views/Content/ProxyApi.cs similarity index 100% rename from back/src/Kyoo.Core/Views/Watch/ProxyApi.cs rename to back/src/Kyoo.Core/Views/Content/ProxyApi.cs diff --git a/back/src/Kyoo.Core/Views/Content/ThumbnailsApi.cs b/back/src/Kyoo.Core/Views/Content/ThumbnailsApi.cs new file mode 100644 index 00000000..e10e4095 --- /dev/null +++ b/back/src/Kyoo.Core/Views/Content/ThumbnailsApi.cs @@ -0,0 +1,59 @@ +// Kyoo - A portable and vast media library solution. +// Copyright (c) Kyoo. +// +// See AUTHORS.md and LICENSE file in the project root for full license information. +// +// Kyoo is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// Kyoo is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kyoo. If not, see . + +using System; +using System.IO; +using Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models; +using Kyoo.Abstractions.Models.Permissions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Kyoo.Core.Api; + +[ApiController] +public class ThumbnailsApi(IThumbnailsManager thumbs) : BaseApi +{ + /// + /// Get Image + /// + /// + /// Get an image from it's id. You can select a specefic quality. + /// + /// The ID of the image to retrive. + /// The quality of the image to retrieve. + /// The image asked. + /// + /// The image does not exists on kyoo. + /// + [HttpGet("{identifier:id}/poster")] + [PartialPermission(Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public IActionResult GetPoster(Guid id, [FromQuery] ImageQuality? quality) + { + string path = thumbs.GetImagePath(id, quality ?? ImageQuality.High); + if (!System.IO.File.Exists(path)) + return NotFound(); + + // Allow clients to cache the image for 6 month. + Response.Headers.CacheControl = $"public, max-age={60 * 60 * 24 * 31 * 6}"; + return PhysicalFile(Path.GetFullPath(path), "image/webp", true); + } +} + diff --git a/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs b/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs index 6ec2470c..20201b5e 100644 --- a/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs +++ b/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs @@ -16,7 +16,7 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System.IO; +using System; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; @@ -28,14 +28,8 @@ using static Kyoo.Abstractions.Models.Utils.Constants; namespace Kyoo.Core.Api; -/// -/// A base class to handle CRUD operations and services thumbnails for -/// a specific resource type . -/// -/// The type of resource to make CRUD and thumbnails apis for. [ApiController] -public class CrudThumbsApi(IRepository repository, IThumbnailsManager thumbs) - : CrudApi(repository) +public class CrudThumbsApi(IRepository repository) : CrudApi(repository) where T : class, IResource, IThumbnails, IQuery { private async Task _GetImage( @@ -50,18 +44,18 @@ public class CrudThumbsApi(IRepository repository, IThumbnailsManager thum ); if (resource == null) return NotFound(); - string path = thumbs.GetImagePath(resource, image, quality ?? ImageQuality.High); - if (!System.IO.File.Exists(path)) + + Image? img = image switch + { + "poster" => resource.Poster, + "thumbnail" => resource.Thumbnail, + "logo" => resource.Logo, + _ => throw new ArgumentException(nameof(image)), + }; + if (img is null) return NotFound(); - if (!identifier.Match(id => false, slug => slug == "random")) - { - // Allow clients to cache the image for 6 month. - Response.Headers.CacheControl = $"public, max-age={60 * 60 * 24 * 31 * 6}"; - } - else - Response.Headers.CacheControl = $"public, no-store"; - return PhysicalFile(Path.GetFullPath(path), "image/webp", true); + return Redirect($"/thumbnails/{img.Id}"); } /// @@ -78,7 +72,7 @@ public class CrudThumbsApi(IRepository repository, IThumbnailsManager thum /// [HttpGet("{identifier:id}/poster")] [PartialPermission(Kind.Read)] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status302Found)] [ProducesResponseType(StatusCodes.Status404NotFound)] public Task GetPoster(Identifier identifier, [FromQuery] ImageQuality? quality) { @@ -99,7 +93,7 @@ public class CrudThumbsApi(IRepository repository, IThumbnailsManager thum /// [HttpGet("{identifier:id}/logo")] [PartialPermission(Kind.Read)] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status302Found)] [ProducesResponseType(StatusCodes.Status404NotFound)] public Task GetLogo(Identifier identifier, [FromQuery] ImageQuality? quality) { @@ -120,6 +114,8 @@ public class CrudThumbsApi(IRepository repository, IThumbnailsManager thum /// [HttpGet("{identifier:id}/thumbnail")] [HttpGet("{identifier:id}/backdrop", Order = AlternativeRoute)] + [ProducesResponseType(StatusCodes.Status302Found)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public Task GetBackdrop(Identifier identifier, [FromQuery] ImageQuality? quality) { return _GetImage(identifier, "thumbnail", quality); diff --git a/back/src/Kyoo.Core/Views/Helper/Transcoder.cs b/back/src/Kyoo.Core/Views/Helper/Transcoder.cs index b6220fe5..337f437b 100644 --- a/back/src/Kyoo.Core/Views/Helper/Transcoder.cs +++ b/back/src/Kyoo.Core/Views/Helper/Transcoder.cs @@ -35,8 +35,7 @@ public static class Transcoder Environment.GetEnvironmentVariable("TRANSCODER_URL") ?? "http://transcoder:7666"; } -public abstract class TranscoderApi(IRepository repository, IThumbnailsManager thumbs) - : CrudThumbsApi(repository, thumbs) +public abstract class TranscoderApi(IRepository repository) : CrudThumbsApi(repository) where T : class, IResource, IThumbnails, IQuery { private Task _Proxy(string route, (string path, string route) info) diff --git a/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs b/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs index c40b1bc6..24acee4a 100644 --- a/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs @@ -40,25 +40,13 @@ namespace Kyoo.Core.Api; [ApiController] [PartialPermission(nameof(Collection))] [ApiDefinition("Collections", Group = ResourcesGroup)] -public class CollectionApi : CrudThumbsApi +public class CollectionApi( + IRepository movies, + IRepository shows, + CollectionRepository collections, + LibraryItemRepository items +) : CrudThumbsApi(collections) { - private readonly ILibraryManager _libraryManager; - private readonly CollectionRepository _collections; - private readonly LibraryItemRepository _items; - - public CollectionApi( - ILibraryManager libraryManager, - CollectionRepository collections, - LibraryItemRepository items, - IThumbnailsManager thumbs - ) - : base(libraryManager.Collections, thumbs) - { - _libraryManager = libraryManager; - _collections = collections; - _items = items; - } - /// /// Add a movie /// @@ -79,14 +67,14 @@ public class CollectionApi : CrudThumbsApi public async Task AddMovie(Identifier identifier, Identifier movie) { Guid collectionId = await identifier.Match( - async id => (await _libraryManager.Collections.Get(id)).Id, - async slug => (await _libraryManager.Collections.Get(slug)).Id + async id => (await collections.Get(id)).Id, + async slug => (await collections.Get(slug)).Id ); Guid movieId = await movie.Match( - async id => (await _libraryManager.Movies.Get(id)).Id, - async slug => (await _libraryManager.Movies.Get(slug)).Id + async id => (await movies.Get(id)).Id, + async slug => (await movies.Get(slug)).Id ); - await _collections.AddMovie(collectionId, movieId); + await collections.AddMovie(collectionId, movieId); return NoContent(); } @@ -110,14 +98,14 @@ public class CollectionApi : CrudThumbsApi public async Task AddShow(Identifier identifier, Identifier show) { Guid collectionId = await identifier.Match( - async id => (await _libraryManager.Collections.Get(id)).Id, - async slug => (await _libraryManager.Collections.Get(slug)).Id + async id => (await collections.Get(id)).Id, + async slug => (await collections.Get(slug)).Id ); Guid showId = await show.Match( - async id => (await _libraryManager.Shows.Get(id)).Id, - async slug => (await _libraryManager.Shows.Get(slug)).Id + async id => (await shows.Get(id)).Id, + async slug => (await shows.Get(slug)).Id ); - await _collections.AddShow(collectionId, showId); + await collections.AddShow(collectionId, showId); return NoContent(); } @@ -151,9 +139,9 @@ public class CollectionApi : CrudThumbsApi { Guid collectionId = await identifier.Match( id => Task.FromResult(id), - async slug => (await _libraryManager.Collections.Get(slug)).Id + async slug => (await collections.Get(slug)).Id ); - ICollection resources = await _items.GetAllOfCollection( + ICollection resources = await items.GetAllOfCollection( collectionId, filter, sortBy == new Sort.Default() @@ -165,8 +153,7 @@ public class CollectionApi : CrudThumbsApi if ( !resources.Any() - && await _libraryManager.Collections.GetOrDefault(identifier.IsSame()) - == null + && await collections.GetOrDefault(identifier.IsSame()) == null ) return NotFound(); return Page(resources, pagination.Limit); @@ -200,7 +187,7 @@ public class CollectionApi : CrudThumbsApi [FromQuery] Include? fields ) { - ICollection resources = await _libraryManager.Shows.GetAll( + ICollection resources = await shows.GetAll( Filter.And(filter, identifier.IsContainedIn(x => x.Collections)), sortBy == new Sort.Default() ? new Sort.By(x => x.AirDate) : sortBy, fields, @@ -209,8 +196,7 @@ public class CollectionApi : CrudThumbsApi if ( !resources.Any() - && await _libraryManager.Collections.GetOrDefault(identifier.IsSame()) - == null + && await collections.GetOrDefault(identifier.IsSame()) == null ) return NotFound(); return Page(resources, pagination.Limit); @@ -244,7 +230,7 @@ public class CollectionApi : CrudThumbsApi [FromQuery] Include? fields ) { - ICollection resources = await _libraryManager.Movies.GetAll( + ICollection resources = await movies.GetAll( Filter.And(filter, identifier.IsContainedIn(x => x.Collections)), sortBy == new Sort.Default() ? new Sort.By(x => x.AirDate) : sortBy, fields, @@ -253,8 +239,7 @@ public class CollectionApi : CrudThumbsApi if ( !resources.Any() - && await _libraryManager.Collections.GetOrDefault(identifier.IsSame()) - == null + && await collections.GetOrDefault(identifier.IsSame()) == null ) return NotFound(); return Page(resources, pagination.Limit); diff --git a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs index f44cdf6e..93b3063c 100644 --- a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs @@ -38,8 +38,8 @@ namespace Kyoo.Core.Api; [ApiController] [PartialPermission(nameof(Episode))] [ApiDefinition("Episodes", Group = ResourcesGroup)] -public class EpisodeApi(ILibraryManager libraryManager, IThumbnailsManager thumbnails) - : TranscoderApi(libraryManager.Episodes, thumbnails) +public class EpisodeApi(ILibraryManager libraryManager) + : TranscoderApi(libraryManager.Episodes) { /// /// Get episode's show diff --git a/back/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs b/back/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs index 9e203375..37f28c26 100644 --- a/back/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/LibraryItemApi.cs @@ -34,23 +34,5 @@ namespace Kyoo.Core.Api; [ApiController] [PartialPermission("LibraryItem")] [ApiDefinition("Items", Group = ResourcesGroup)] -public class LibraryItemApi : CrudThumbsApi -{ - /// - /// The library item repository used to modify or retrieve information in the data store. - /// - private readonly IRepository _libraryItems; - - /// - /// Create a new . - /// - /// - /// The library item repository used to modify or retrieve information in the data store. - /// - /// Thumbnail manager to retrieve images. - public LibraryItemApi(IRepository libraryItems, IThumbnailsManager thumbs) - : base(libraryItems, thumbs) - { - _libraryItems = libraryItems; - } -} +public class LibraryItemApi(IRepository libraryItems) + : CrudThumbsApi(libraryItems) { } diff --git a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs index fddc00b0..03f07822 100644 --- a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs @@ -40,8 +40,8 @@ namespace Kyoo.Core.Api; [ApiController] [PartialPermission(nameof(Show))] [ApiDefinition("Shows", Group = ResourcesGroup)] -public class MovieApi(ILibraryManager libraryManager, IThumbnailsManager thumbs) - : TranscoderApi(libraryManager.Movies, thumbs) +public class MovieApi(ILibraryManager libraryManager) + : TranscoderApi(libraryManager.Movies) { /// /// Get studio that made the show diff --git a/back/src/Kyoo.Core/Views/Resources/NewsApi.cs b/back/src/Kyoo.Core/Views/Resources/NewsApi.cs index 56153d6b..84aa8f3b 100644 --- a/back/src/Kyoo.Core/Views/Resources/NewsApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/NewsApi.cs @@ -33,8 +33,4 @@ namespace Kyoo.Core.Api; [ApiController] [PartialPermission("LibraryItem")] [ApiDefinition("News", Group = ResourcesGroup)] -public class NewsApi : CrudThumbsApi -{ - public NewsApi(IRepository news, IThumbnailsManager thumbs) - : base(news, thumbs) { } -} +public class NewsApi(IRepository news) : CrudThumbsApi(news) { } diff --git a/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs b/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs index d12c2273..8df85291 100644 --- a/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs @@ -38,26 +38,9 @@ namespace Kyoo.Core.Api; [ApiController] [PartialPermission(nameof(Season))] [ApiDefinition("Seasons", Group = ResourcesGroup)] -public class SeasonApi : CrudThumbsApi +public class SeasonApi(ILibraryManager libraryManager) + : CrudThumbsApi(libraryManager.Seasons) { - /// - /// The library manager used to modify or retrieve information in the data store. - /// - private readonly ILibraryManager _libraryManager; - - /// - /// Create a new . - /// - /// - /// The library manager used to modify or retrieve information in the data store. - /// - /// The thumbnail manager used to retrieve images paths. - public SeasonApi(ILibraryManager libraryManager, IThumbnailsManager thumbs) - : base(libraryManager.Seasons, thumbs) - { - _libraryManager = libraryManager; - } - /// /// Get episodes in the season /// @@ -86,7 +69,7 @@ public class SeasonApi : CrudThumbsApi [FromQuery] Include fields ) { - ICollection resources = await _libraryManager.Episodes.GetAll( + ICollection resources = await libraryManager.Episodes.GetAll( Filter.And(filter, identifier.Matcher(x => x.SeasonId, x => x.Season!.Slug)), sortBy, fields, @@ -95,7 +78,7 @@ public class SeasonApi : CrudThumbsApi if ( !resources.Any() - && await _libraryManager.Seasons.GetOrDefault(identifier.IsSame()) == null + && await libraryManager.Seasons.GetOrDefault(identifier.IsSame()) == null ) return NotFound(); return Page(resources, pagination.Limit); @@ -120,7 +103,7 @@ public class SeasonApi : CrudThumbsApi [FromQuery] Include fields ) { - Show? ret = await _libraryManager.Shows.GetOrDefault( + Show? ret = await libraryManager.Shows.GetOrDefault( identifier.IsContainedIn(x => x.Seasons!), fields ); diff --git a/back/src/Kyoo.Core/Views/Resources/ShowApi.cs b/back/src/Kyoo.Core/Views/Resources/ShowApi.cs index 0946e2c8..54138ac9 100644 --- a/back/src/Kyoo.Core/Views/Resources/ShowApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/ShowApi.cs @@ -40,26 +40,8 @@ namespace Kyoo.Core.Api; [ApiController] [PartialPermission(nameof(Show))] [ApiDefinition("Shows", Group = ResourcesGroup)] -public class ShowApi : CrudThumbsApi +public class ShowApi(ILibraryManager libraryManager) : CrudThumbsApi(libraryManager.Shows) { - /// - /// The library manager used to modify or retrieve information in the data store. - /// - private readonly ILibraryManager _libraryManager; - - /// - /// Create a new . - /// - /// - /// The library manager used to modify or retrieve information about the data store. - /// - /// The thumbnail manager used to retrieve images paths. - public ShowApi(ILibraryManager libraryManager, IThumbnailsManager thumbs) - : base(libraryManager.Shows, thumbs) - { - _libraryManager = libraryManager; - } - /// /// Get seasons of this show /// @@ -88,7 +70,7 @@ public class ShowApi : CrudThumbsApi [FromQuery] Include fields ) { - ICollection resources = await _libraryManager.Seasons.GetAll( + ICollection resources = await libraryManager.Seasons.GetAll( Filter.And(filter, identifier.Matcher(x => x.ShowId, x => x.Show!.Slug)), sortBy, fields, @@ -97,7 +79,7 @@ public class ShowApi : CrudThumbsApi if ( !resources.Any() - && await _libraryManager.Shows.GetOrDefault(identifier.IsSame()) == null + && await libraryManager.Shows.GetOrDefault(identifier.IsSame()) == null ) return NotFound(); return Page(resources, pagination.Limit); @@ -131,7 +113,7 @@ public class ShowApi : CrudThumbsApi [FromQuery] Include fields ) { - ICollection resources = await _libraryManager.Episodes.GetAll( + ICollection resources = await libraryManager.Episodes.GetAll( Filter.And(filter, identifier.Matcher(x => x.ShowId, x => x.Show!.Slug)), sortBy, fields, @@ -140,7 +122,7 @@ public class ShowApi : CrudThumbsApi if ( !resources.Any() - && await _libraryManager.Shows.GetOrDefault(identifier.IsSame()) == null + && await libraryManager.Shows.GetOrDefault(identifier.IsSame()) == null ) return NotFound(); return Page(resources, pagination.Limit); @@ -165,7 +147,7 @@ public class ShowApi : CrudThumbsApi [FromQuery] Include fields ) { - return await _libraryManager.Studios.Get( + return await libraryManager.Studios.Get( identifier.IsContainedIn(x => x.Shows!), fields ); @@ -199,7 +181,7 @@ public class ShowApi : CrudThumbsApi [FromQuery] Include fields ) { - ICollection resources = await _libraryManager.Collections.GetAll( + ICollection resources = await libraryManager.Collections.GetAll( Filter.And(filter, identifier.IsContainedIn(x => x.Shows!)), sortBy, fields, @@ -208,7 +190,7 @@ public class ShowApi : CrudThumbsApi if ( !resources.Any() - && await _libraryManager.Shows.GetOrDefault(identifier.IsSame()) == null + && await libraryManager.Shows.GetOrDefault(identifier.IsSame()) == null ) return NotFound(); return Page(resources, pagination.Limit); @@ -233,9 +215,9 @@ public class ShowApi : CrudThumbsApi { Guid id = await identifier.Match( id => Task.FromResult(id), - async slug => (await _libraryManager.Shows.Get(slug)).Id + async slug => (await libraryManager.Shows.Get(slug)).Id ); - return await _libraryManager.WatchStatus.GetShowStatus(id, User.GetIdOrThrow()); + return await libraryManager.WatchStatus.GetShowStatus(id, User.GetIdOrThrow()); } /// @@ -260,9 +242,9 @@ public class ShowApi : CrudThumbsApi { Guid id = await identifier.Match( id => Task.FromResult(id), - async slug => (await _libraryManager.Shows.Get(slug)).Id + async slug => (await libraryManager.Shows.Get(slug)).Id ); - return await _libraryManager.WatchStatus.SetShowStatus(id, User.GetIdOrThrow(), status); + return await libraryManager.WatchStatus.SetShowStatus(id, User.GetIdOrThrow(), status); } /// @@ -283,8 +265,8 @@ public class ShowApi : CrudThumbsApi { Guid id = await identifier.Match( id => Task.FromResult(id), - async slug => (await _libraryManager.Shows.Get(slug)).Id + async slug => (await libraryManager.Shows.Get(slug)).Id ); - await _libraryManager.WatchStatus.DeleteShowStatus(id, User.GetIdOrThrow()); + await libraryManager.WatchStatus.DeleteShowStatus(id, User.GetIdOrThrow()); } } From fb4424fb6b1055532c993bb07aa8cde361e90d7e Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 20 Apr 2024 14:10:50 +0200 Subject: [PATCH 09/26] Restore custom out path needed for csharp lsp to work --- back/Dockerfile.migrations | 5 ++++- back/src/Directory.Build.props | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/back/Dockerfile.migrations b/back/Dockerfile.migrations index 55377552..2278e642 100644 --- a/back/Dockerfile.migrations +++ b/back/Dockerfile.migrations @@ -19,7 +19,10 @@ RUN dotnet restore -a $TARGETARCH COPY . . RUN dotnet build -RUN dotnet ef migrations bundle --no-build --self-contained -r linux-${TARGETARCH} -f -o /app/migrate -p src/Kyoo.Postgresql --verbose +RUN dotnet ef migrations bundle \ + --msbuildprojectextensionspath out/obj/Kyoo.Postgresql \ + --no-build --self-contained -r linux-${TARGETARCH} -f \ + -o /app/migrate -p src/Kyoo.Postgresql --verbose FROM mcr.microsoft.com/dotnet/runtime-deps:8.0 COPY --from=builder /app/migrate /app/migrate diff --git a/back/src/Directory.Build.props b/back/src/Directory.Build.props index 3b9f144a..76832704 100644 --- a/back/src/Directory.Build.props +++ b/back/src/Directory.Build.props @@ -28,6 +28,11 @@ true + + $(MsBuildThisFileDirectory)/../out/obj/$(MSBuildProjectName) + $(MsBuildThisFileDirectory)/../out/bin/$(MSBuildProjectName) + + From f15b92aae1855be47855c294b19caa8d155f6bbf Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 01:53:33 +0200 Subject: [PATCH 10/26] Floor seconds instead of rounding in the player UI --- front/packages/ui/src/player/components/left-buttons.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/packages/ui/src/player/components/left-buttons.tsx b/front/packages/ui/src/player/components/left-buttons.tsx index 93dbf27c..0f61411b 100644 --- a/front/packages/ui/src/player/components/left-buttons.tsx +++ b/front/packages/ui/src/player/components/left-buttons.tsx @@ -208,7 +208,7 @@ export const toTimerString = (timer?: number, duration?: number) => { return "??:??"; const h = Math.floor(timer / 3600); const min = Math.floor((timer / 60) % 60); - const sec = Math.round(timer % 60); + const sec = Math.floor(timer % 60); const fmt = (n: number) => n.toString().padStart(2, "0"); if (duration >= 3600) return `${fmt(h)}:${fmt(min)}:${fmt(sec)}`; From c576babde8f542837ecc969b8f07edddab50b57e Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 01:53:53 +0200 Subject: [PATCH 11/26] Migrate images --- .../Resources/Interfaces/IThumbnails.cs | 9 +- back/src/Kyoo.Postgresql/DatabaseContext.cs | 130 -- .../20240420124608_ReworkImages.Designer.cs | 1375 +++++++++++++++++ .../Migrations/20240420124608_ReworkImages.cs | 460 ++++++ .../PostgresContextModelSnapshot.cs | 215 +-- 5 files changed, 1966 insertions(+), 223 deletions(-) create mode 100644 back/src/Kyoo.Postgresql/Migrations/20240420124608_ReworkImages.Designer.cs create mode 100644 back/src/Kyoo.Postgresql/Migrations/20240420124608_ReworkImages.cs diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs index 2bbffa72..92402690 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs @@ -88,6 +88,7 @@ public class Image Blurhash = blurhash ?? "000000"; } + // public class ImageConvertor : JsonConverter { /// @@ -100,7 +101,13 @@ public class Image if (reader.TokenType == JsonTokenType.String && reader.GetString() is string source) return new Image(source); using JsonDocument document = JsonDocument.ParseValue(ref reader); - return document.RootElement.Deserialize(); + string? src = document.RootElement.GetProperty("Source").GetString(); + string? blurhash = document.RootElement.GetProperty("Blurhash").GetString(); + Guid? id = document.RootElement.GetProperty("Id").GetGuid(); + return new Image(src ?? string.Empty, blurhash) + { + Id = id ?? Guid.Empty + }; } /// diff --git a/back/src/Kyoo.Postgresql/DatabaseContext.cs b/back/src/Kyoo.Postgresql/DatabaseContext.cs index 320021d3..3f2cd14c 100644 --- a/back/src/Kyoo.Postgresql/DatabaseContext.cs +++ b/back/src/Kyoo.Postgresql/DatabaseContext.cs @@ -23,7 +23,6 @@ using System.Linq.Expressions; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Authentication; @@ -33,13 +32,6 @@ using Microsoft.EntityFrameworkCore.ChangeTracking; namespace Kyoo.Postgresql; -/// -/// The database handle used for all local repositories. -/// This is an abstract class. It is meant to be implemented by plugins. This allow the core to be database agnostic. -/// -/// -/// It should not be used directly, to access the database use a or repositories. -/// public abstract class DatabaseContext : DbContext { private readonly IHttpContextAccessor _accessor; @@ -53,39 +45,18 @@ public abstract class DatabaseContext : DbContext public Guid? CurrentUserId => _accessor.HttpContext?.User.GetId(); - /// - /// All collections of Kyoo. See . - /// public DbSet Collections { get; set; } - /// - /// All movies of Kyoo. See . - /// public DbSet Movies { get; set; } - /// - /// All shows of Kyoo. See . - /// public DbSet Shows { get; set; } - /// - /// All seasons of Kyoo. See . - /// public DbSet Seasons { get; set; } - /// - /// All episodes of Kyoo. See . - /// public DbSet Episodes { get; set; } - /// - /// All studios of Kyoo. See . - /// public DbSet Studios { get; set; } - /// - /// The list of registered users. - /// public DbSet Users { get; set; } public DbSet MovieWatchStatus { get; set; } @@ -129,28 +100,13 @@ public abstract class DatabaseContext : DbContext _accessor = accessor; } - /// - /// Get the name of the link table of the two given types. - /// - /// The owner type of the relation - /// The child type of the relation - /// The name of the table containing the links. protected abstract string LinkName() where T : IResource where T2 : IResource; - /// - /// Get the name of a link's foreign key. - /// - /// The type that will be accessible via the navigation - /// The name of the foreign key for the given resource. protected abstract string LinkNameFk() where T : IResource; - /// - /// Set basic configurations (like preventing query tracking) - /// - /// An option builder to fill. protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); @@ -227,15 +183,6 @@ public abstract class DatabaseContext : DbContext .ValueGeneratedOnAdd(); } - /// - /// Create a many to many relationship between the two entities. - /// The resulting relationship will have an available method. - /// - /// The database model builder - /// The first navigation expression from T to T2 - /// The second navigation expression from T2 to T - /// The owning type of the relationship - /// The owned type of the relationship private void _HasManyToMany( ModelBuilder modelBuilder, Expression?>> firstNavigation, @@ -263,10 +210,6 @@ public abstract class DatabaseContext : DbContext ); } - /// - /// Set database parameters to support every types of Kyoo. - /// - /// The database's model builder. protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -412,28 +355,6 @@ public abstract class DatabaseContext : DbContext _HasJson(modelBuilder, x => x.Extra); } - /// - /// Return a new or an in cache temporary object wih the same ID as the one given - /// - /// If a resource with the same ID is found in the database, it will be used. - /// will be used otherwise - /// The type of the resource - /// A resource that is now tracked by this context. - public T GetTemporaryObject(T model) - where T : class, IResource - { - T? tmp = Set().Local.FirstOrDefault(x => x.Id == model.Id); - if (tmp != null) - return tmp; - Entry(model).State = EntityState.Unchanged; - return model; - } - - /// - /// Save changes that are applied to this context. - /// - /// A duplicated item has been found. - /// The number of state entries written to the database. public override int SaveChanges() { try @@ -449,13 +370,6 @@ public abstract class DatabaseContext : DbContext } } - /// - /// Save changes that are applied to this context. - /// - /// Indicates whether AcceptAllChanges() is called after the changes - /// have been sent successfully to the database. - /// A duplicated item has been found. - /// The number of state entries written to the database. public override int SaveChanges(bool acceptAllChangesOnSuccess) { try @@ -471,14 +385,6 @@ public abstract class DatabaseContext : DbContext } } - /// - /// Save changes that are applied to this context. - /// - /// Indicates whether AcceptAllChanges() is called after the changes - /// have been sent successfully to the database. - /// A to observe while waiting for the task to complete - /// A duplicated item has been found. - /// The number of state entries written to the database. public override async Task SaveChangesAsync( bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default @@ -497,12 +403,6 @@ public abstract class DatabaseContext : DbContext } } - /// - /// Save changes that are applied to this context. - /// - /// A to observe while waiting for the task to complete - /// A duplicated item has been found. - /// The number of state entries written to the database. public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { try @@ -518,14 +418,6 @@ public abstract class DatabaseContext : DbContext } } - /// - /// Save changes that are applied to this context. - /// - /// How to retrieve the conflicting item. - /// A to observe while waiting for the task to complete - /// A duplicated item has been found. - /// The type of the potential duplicate (this is unused). - /// The number of state entries written to the database. public async Task SaveChangesAsync( Func> getExisting, CancellationToken cancellationToken = default @@ -548,12 +440,6 @@ public abstract class DatabaseContext : DbContext } } - /// - /// Save changes if no duplicates are found. If one is found, no change are saved but the current changes are no discarded. - /// The current context will still hold those invalid changes. - /// - /// A to observe while waiting for the task to complete - /// The number of state entries written to the database or -1 if a duplicate exist. public async Task SaveIfNoDuplicates(CancellationToken cancellationToken = default) { try @@ -566,30 +452,14 @@ public abstract class DatabaseContext : DbContext } } - /// - /// Return the first resource with the given slug that is currently tracked by this context. - /// This allow one to limit redundant calls to during the - /// same transaction and prevent fails from EF when two same entities are being tracked. - /// - /// The slug of the resource to check - /// The type of entity to check - /// The local entity representing the resource with the given slug if it exists or null. public T? LocalEntity(string slug) where T : class, IResource { return ChangeTracker.Entries().FirstOrDefault(x => x.Entity.Slug == slug)?.Entity; } - /// - /// Check if the exception is a duplicated exception. - /// - /// The exception to check - /// True if the exception is a duplicate exception. False otherwise protected abstract bool IsDuplicateException(Exception ex); - /// - /// Delete every changes that are on this context. - /// public void DiscardChanges() { foreach ( diff --git a/back/src/Kyoo.Postgresql/Migrations/20240420124608_ReworkImages.Designer.cs b/back/src/Kyoo.Postgresql/Migrations/20240420124608_ReworkImages.Designer.cs new file mode 100644 index 00000000..0157c3cd --- /dev/null +++ b/back/src/Kyoo.Postgresql/Migrations/20240420124608_ReworkImages.Designer.cs @@ -0,0 +1,1375 @@ +// +using System; +using System.Collections.Generic; +using Kyoo.Abstractions.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Kyoo.Postgresql.Migrations +{ + [DbContext(typeof(PostgresContext))] + [Migration("20240420124608_ReworkImages")] + partial class ReworkImages + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "genre", new[] { "action", "adventure", "animation", "comedy", "crime", "documentary", "drama", "family", "fantasy", "history", "horror", "music", "mystery", "romance", "science_fiction", "thriller", "war", "western", "kids", "news", "reality", "soap", "talk", "politics" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "status", new[] { "unknown", "finished", "airing", "planned" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "watch_status", new[] { "completed", "watching", "droped", "planned", "deleted" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Collection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AddedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("added_date") + .HasDefaultValueSql("now() at time zone 'utc'"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("NextMetadataRefresh") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("next_metadata_refresh") + .HasDefaultValueSql("now() at time zone 'utc' + interval '2 hours'"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_collections"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_collections_slug"); + + b.ToTable("collections", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Episode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AbsoluteNumber") + .HasColumnType("integer") + .HasColumnName("absolute_number"); + + b.Property("AddedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("added_date") + .HasDefaultValueSql("now() at time zone 'utc'"); + + b.Property("EpisodeNumber") + .HasColumnType("integer") + .HasColumnName("episode_number"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("NextMetadataRefresh") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("next_metadata_refresh") + .HasDefaultValueSql("now() at time zone 'utc' + interval '2 hours'"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("ReleaseDate") + .HasColumnType("date") + .HasColumnName("release_date"); + + b.Property("Runtime") + .HasColumnType("integer") + .HasColumnName("runtime"); + + b.Property("SeasonId") + .HasColumnType("uuid") + .HasColumnName("season_id"); + + b.Property("SeasonNumber") + .HasColumnType("integer") + .HasColumnName("season_number"); + + b.Property("ShowId") + .HasColumnType("uuid") + .HasColumnName("show_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_episodes"); + + b.HasIndex("SeasonId") + .HasDatabaseName("ix_episodes_season_id"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_episodes_slug"); + + b.HasIndex("ShowId", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") + .IsUnique() + .HasDatabaseName("ix_episodes_show_id_season_number_episode_number_absolute_numb"); + + b.ToTable("episodes", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.EpisodeWatchStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("EpisodeId") + .HasColumnType("uuid") + .HasColumnName("episode_id"); + + b.Property("AddedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("added_date") + .HasDefaultValueSql("now() at time zone 'utc'"); + + b.Property("PlayedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("played_date"); + + b.Property("Status") + .HasColumnType("watch_status") + .HasColumnName("status"); + + b.Property("WatchedPercent") + .HasColumnType("integer") + .HasColumnName("watched_percent"); + + b.Property("WatchedTime") + .HasColumnType("integer") + .HasColumnName("watched_time"); + + b.HasKey("UserId", "EpisodeId") + .HasName("pk_episode_watch_status"); + + b.HasIndex("EpisodeId") + .HasDatabaseName("ix_episode_watch_status_episode_id"); + + b.ToTable("episode_watch_status", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Issue", b => + { + b.Property("Domain") + .HasColumnType("text") + .HasColumnName("domain"); + + b.Property("Cause") + .HasColumnType("text") + .HasColumnName("cause"); + + b.Property("AddedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("added_date") + .HasDefaultValueSql("now() at time zone 'utc'"); + + b.Property("Extra") + .IsRequired() + .HasColumnType("json") + .HasColumnName("extra"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.HasKey("Domain", "Cause") + .HasName("pk_issues"); + + b.ToTable("issues", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AddedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("added_date") + .HasDefaultValueSql("now() at time zone 'utc'"); + + b.Property("AirDate") + .HasColumnType("date") + .HasColumnName("air_date"); + + b.Property("Aliases") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("aliases"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property>("Genres") + .IsRequired() + .HasColumnType("genre[]") + .HasColumnName("genres"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("NextMetadataRefresh") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("next_metadata_refresh") + .HasDefaultValueSql("now() at time zone 'utc' + interval '2 hours'"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("Rating") + .HasColumnType("integer") + .HasColumnName("rating"); + + b.Property("Runtime") + .HasColumnType("integer") + .HasColumnName("runtime"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("Status") + .HasColumnType("status") + .HasColumnName("status"); + + b.Property("StudioId") + .HasColumnType("uuid") + .HasColumnName("studio_id"); + + b.Property("Tagline") + .HasColumnType("text") + .HasColumnName("tagline"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("tags"); + + b.Property("Trailer") + .HasColumnType("text") + .HasColumnName("trailer"); + + b.HasKey("Id") + .HasName("pk_movies"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_movies_slug"); + + b.HasIndex("StudioId") + .HasDatabaseName("ix_movies_studio_id"); + + b.ToTable("movies", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.MovieWatchStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("MovieId") + .HasColumnType("uuid") + .HasColumnName("movie_id"); + + b.Property("AddedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("added_date") + .HasDefaultValueSql("now() at time zone 'utc'"); + + b.Property("PlayedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("played_date"); + + b.Property("Status") + .HasColumnType("watch_status") + .HasColumnName("status"); + + b.Property("WatchedPercent") + .HasColumnType("integer") + .HasColumnName("watched_percent"); + + b.Property("WatchedTime") + .HasColumnType("integer") + .HasColumnName("watched_time"); + + b.HasKey("UserId", "MovieId") + .HasName("pk_movie_watch_status"); + + b.HasIndex("MovieId") + .HasDatabaseName("ix_movie_watch_status_movie_id"); + + b.ToTable("movie_watch_status", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AddedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("added_date") + .HasDefaultValueSql("now() at time zone 'utc'"); + + b.Property("EndDate") + .HasColumnType("date") + .HasColumnName("end_date"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("NextMetadataRefresh") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("next_metadata_refresh") + .HasDefaultValueSql("now() at time zone 'utc' + interval '2 hours'"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("SeasonNumber") + .HasColumnType("integer") + .HasColumnName("season_number"); + + b.Property("ShowId") + .HasColumnType("uuid") + .HasColumnName("show_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("StartDate") + .HasColumnType("date") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("pk_seasons"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_seasons_slug"); + + b.HasIndex("ShowId", "SeasonNumber") + .IsUnique() + .HasDatabaseName("ix_seasons_show_id_season_number"); + + b.ToTable("seasons", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AddedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("added_date") + .HasDefaultValueSql("now() at time zone 'utc'"); + + b.Property>("Aliases") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("aliases"); + + b.Property("EndAir") + .HasColumnType("date") + .HasColumnName("end_air"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property>("Genres") + .IsRequired() + .HasColumnType("genre[]") + .HasColumnName("genres"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("NextMetadataRefresh") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("next_metadata_refresh") + .HasDefaultValueSql("now() at time zone 'utc' + interval '2 hours'"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Rating") + .HasColumnType("integer") + .HasColumnName("rating"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("StartAir") + .HasColumnType("date") + .HasColumnName("start_air"); + + b.Property("Status") + .HasColumnType("status") + .HasColumnName("status"); + + b.Property("StudioId") + .HasColumnType("uuid") + .HasColumnName("studio_id"); + + b.Property("Tagline") + .HasColumnType("text") + .HasColumnName("tagline"); + + b.Property>("Tags") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("tags"); + + b.Property("Trailer") + .HasColumnType("text") + .HasColumnName("trailer"); + + b.HasKey("Id") + .HasName("pk_shows"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_shows_slug"); + + b.HasIndex("StudioId") + .HasDatabaseName("ix_shows_studio_id"); + + b.ToTable("shows", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.ShowWatchStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("ShowId") + .HasColumnType("uuid") + .HasColumnName("show_id"); + + b.Property("AddedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("added_date") + .HasDefaultValueSql("now() at time zone 'utc'"); + + b.Property("NextEpisodeId") + .HasColumnType("uuid") + .HasColumnName("next_episode_id"); + + b.Property("PlayedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("played_date"); + + b.Property("Status") + .HasColumnType("watch_status") + .HasColumnName("status"); + + b.Property("UnseenEpisodesCount") + .HasColumnType("integer") + .HasColumnName("unseen_episodes_count"); + + b.Property("WatchedPercent") + .HasColumnType("integer") + .HasColumnName("watched_percent"); + + b.Property("WatchedTime") + .HasColumnType("integer") + .HasColumnName("watched_time"); + + b.HasKey("UserId", "ShowId") + .HasName("pk_show_watch_status"); + + b.HasIndex("NextEpisodeId") + .HasDatabaseName("ix_show_watch_status_next_episode_id"); + + b.HasIndex("ShowId") + .HasDatabaseName("ix_show_watch_status_show_id"); + + b.ToTable("show_watch_status", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Studio", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_studios"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_studios_slug"); + + b.ToTable("studios", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AddedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("added_date") + .HasDefaultValueSql("now() at time zone 'utc'"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("json") + .HasColumnName("external_id"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Permissions") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("permissions"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("json") + .HasColumnName("settings"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("slug"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_users_slug"); + + b.HasIndex("Username") + .IsUnique() + .HasDatabaseName("ix_users_username"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("link_collection_movie", b => + { + b.Property("collection_id") + .HasColumnType("uuid") + .HasColumnName("collection_id"); + + b.Property("movie_id") + .HasColumnType("uuid") + .HasColumnName("movie_id"); + + b.HasKey("collection_id", "movie_id") + .HasName("pk_link_collection_movie"); + + b.HasIndex("movie_id") + .HasDatabaseName("ix_link_collection_movie_movie_id"); + + b.ToTable("link_collection_movie", (string)null); + }); + + modelBuilder.Entity("link_collection_show", b => + { + b.Property("collection_id") + .HasColumnType("uuid") + .HasColumnName("collection_id"); + + b.Property("show_id") + .HasColumnType("uuid") + .HasColumnName("show_id"); + + b.HasKey("collection_id", "show_id") + .HasName("pk_link_collection_show"); + + b.HasIndex("show_id") + .HasDatabaseName("ix_link_collection_show_show_id"); + + b.ToTable("link_collection_show", (string)null); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Collection", b => + { + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("CollectionId") + .HasColumnType("uuid"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("CollectionId"); + + b1.ToTable("collections"); + + b1.ToJson("logo"); + + b1.WithOwner() + .HasForeignKey("CollectionId") + .HasConstraintName("fk_collections_collections_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("CollectionId") + .HasColumnType("uuid"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("CollectionId") + .HasName("pk_collections"); + + b1.ToTable("collections"); + + b1.ToJson("poster"); + + b1.WithOwner() + .HasForeignKey("CollectionId") + .HasConstraintName("fk_collections_collections_collection_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("CollectionId") + .HasColumnType("uuid"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("CollectionId"); + + b1.ToTable("collections"); + + b1.ToJson("thumbnail"); + + b1.WithOwner() + .HasForeignKey("CollectionId") + .HasConstraintName("fk_collections_collections_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Episode", b => + { + b.HasOne("Kyoo.Abstractions.Models.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_episodes_seasons_season_id"); + + b.HasOne("Kyoo.Abstractions.Models.Show", "Show") + .WithMany("Episodes") + .HasForeignKey("ShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_episodes_shows_show_id"); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("EpisodeId") + .HasColumnType("uuid"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("EpisodeId"); + + b1.ToTable("episodes"); + + b1.ToJson("logo"); + + b1.WithOwner() + .HasForeignKey("EpisodeId") + .HasConstraintName("fk_episodes_episodes_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("EpisodeId") + .HasColumnType("uuid"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("EpisodeId"); + + b1.ToTable("episodes"); + + b1.ToJson("poster"); + + b1.WithOwner() + .HasForeignKey("EpisodeId") + .HasConstraintName("fk_episodes_episodes_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("EpisodeId") + .HasColumnType("uuid"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("EpisodeId"); + + b1.ToTable("episodes"); + + b1.ToJson("thumbnail"); + + b1.WithOwner() + .HasForeignKey("EpisodeId") + .HasConstraintName("fk_episodes_episodes_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Season"); + + b.Navigation("Show"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.EpisodeWatchStatus", b => + { + b.HasOne("Kyoo.Abstractions.Models.Episode", "Episode") + .WithMany("Watched") + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_episode_watch_status_episodes_episode_id"); + + b.HasOne("Kyoo.Abstractions.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_episode_watch_status_users_user_id"); + + b.Navigation("Episode"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Movie", b => + { + b.HasOne("Kyoo.Abstractions.Models.Studio", "Studio") + .WithMany("Movies") + .HasForeignKey("StudioId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_movies_studios_studio_id"); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("MovieId") + .HasColumnType("uuid"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("MovieId"); + + b1.ToTable("movies"); + + b1.ToJson("logo"); + + b1.WithOwner() + .HasForeignKey("MovieId") + .HasConstraintName("fk_movies_movies_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("MovieId") + .HasColumnType("uuid"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("MovieId"); + + b1.ToTable("movies"); + + b1.ToJson("poster"); + + b1.WithOwner() + .HasForeignKey("MovieId") + .HasConstraintName("fk_movies_movies_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("MovieId") + .HasColumnType("uuid"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("MovieId"); + + b1.ToTable("movies"); + + b1.ToJson("thumbnail"); + + b1.WithOwner() + .HasForeignKey("MovieId") + .HasConstraintName("fk_movies_movies_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Studio"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.MovieWatchStatus", b => + { + b.HasOne("Kyoo.Abstractions.Models.Movie", "Movie") + .WithMany("Watched") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_movie_watch_status_movies_movie_id"); + + b.HasOne("Kyoo.Abstractions.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_movie_watch_status_users_user_id"); + + b.Navigation("Movie"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => + { + b.HasOne("Kyoo.Abstractions.Models.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_seasons_shows_show_id"); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("SeasonId") + .HasColumnType("uuid"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("SeasonId"); + + b1.ToTable("seasons"); + + b1.ToJson("logo"); + + b1.WithOwner() + .HasForeignKey("SeasonId") + .HasConstraintName("fk_seasons_seasons_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("SeasonId") + .HasColumnType("uuid"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("SeasonId"); + + b1.ToTable("seasons"); + + b1.ToJson("poster"); + + b1.WithOwner() + .HasForeignKey("SeasonId") + .HasConstraintName("fk_seasons_seasons_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("SeasonId") + .HasColumnType("uuid"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("SeasonId"); + + b1.ToTable("seasons"); + + b1.ToJson("thumbnail"); + + b1.WithOwner() + .HasForeignKey("SeasonId") + .HasConstraintName("fk_seasons_seasons_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Show"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => + { + b.HasOne("Kyoo.Abstractions.Models.Studio", "Studio") + .WithMany("Shows") + .HasForeignKey("StudioId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_shows_studios_studio_id"); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => + { + b1.Property("ShowId") + .HasColumnType("uuid"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("ShowId"); + + b1.ToTable("shows"); + + b1.ToJson("logo"); + + b1.WithOwner() + .HasForeignKey("ShowId") + .HasConstraintName("fk_shows_shows_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => + { + b1.Property("ShowId") + .HasColumnType("uuid"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("ShowId"); + + b1.ToTable("shows"); + + b1.ToJson("poster"); + + b1.WithOwner() + .HasForeignKey("ShowId") + .HasConstraintName("fk_shows_shows_id"); + }); + + b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => + { + b1.Property("ShowId") + .HasColumnType("uuid"); + + b1.Property("Blurhash") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); + + b1.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("ShowId"); + + b1.ToTable("shows"); + + b1.ToJson("thumbnail"); + + b1.WithOwner() + .HasForeignKey("ShowId") + .HasConstraintName("fk_shows_shows_id"); + }); + + b.Navigation("Logo"); + + b.Navigation("Poster"); + + b.Navigation("Studio"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.ShowWatchStatus", b => + { + b.HasOne("Kyoo.Abstractions.Models.Episode", "NextEpisode") + .WithMany() + .HasForeignKey("NextEpisodeId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_show_watch_status_episodes_next_episode_id"); + + b.HasOne("Kyoo.Abstractions.Models.Show", "Show") + .WithMany("Watched") + .HasForeignKey("ShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_show_watch_status_shows_show_id"); + + b.HasOne("Kyoo.Abstractions.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_show_watch_status_users_user_id"); + + b.Navigation("NextEpisode"); + + b.Navigation("Show"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("link_collection_movie", b => + { + b.HasOne("Kyoo.Abstractions.Models.Collection", null) + .WithMany() + .HasForeignKey("collection_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_collection_movie_collections_collection_id"); + + b.HasOne("Kyoo.Abstractions.Models.Movie", null) + .WithMany() + .HasForeignKey("movie_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_collection_movie_movies_movie_id"); + }); + + modelBuilder.Entity("link_collection_show", b => + { + b.HasOne("Kyoo.Abstractions.Models.Collection", null) + .WithMany() + .HasForeignKey("collection_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_collection_show_collections_collection_id"); + + b.HasOne("Kyoo.Abstractions.Models.Show", null) + .WithMany() + .HasForeignKey("show_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_link_collection_show_shows_show_id"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Episode", b => + { + b.Navigation("Watched"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Movie", b => + { + b.Navigation("Watched"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Season", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Show", b => + { + b.Navigation("Episodes"); + + b.Navigation("Seasons"); + + b.Navigation("Watched"); + }); + + modelBuilder.Entity("Kyoo.Abstractions.Models.Studio", b => + { + b.Navigation("Movies"); + + b.Navigation("Shows"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/back/src/Kyoo.Postgresql/Migrations/20240420124608_ReworkImages.cs b/back/src/Kyoo.Postgresql/Migrations/20240420124608_ReworkImages.cs new file mode 100644 index 00000000..9606e2d7 --- /dev/null +++ b/back/src/Kyoo.Postgresql/Migrations/20240420124608_ReworkImages.cs @@ -0,0 +1,460 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Kyoo.Postgresql.Migrations +{ + /// + public partial class ReworkImages : Migration + { + private void MigrateImage(MigrationBuilder migrationBuilder, string table, string type) + { + migrationBuilder.Sql($""" + update {table} as r set {type} = json_build_object( + 'Id', gen_random_uuid(), + 'Source', r.{type}_source, + 'Blurhash', r.{type}_blurhash + ) + where r.{type}_source is not null + """); + } + + private void UnMigrateImage(MigrationBuilder migrationBuilder, string table, string type) + { + migrationBuilder.Sql($""" + update {table} as r + set {type}_source = r.{type}->>'Source', + {type}_blurhash = r.{type}->>'Blurhash' + """); + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "logo", + table: "shows", + type: "jsonb", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "poster", + table: "shows", + type: "jsonb", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "thumbnail", + table: "shows", + type: "jsonb", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "logo", + table: "seasons", + type: "jsonb", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "poster", + table: "seasons", + type: "jsonb", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "thumbnail", + table: "seasons", + type: "jsonb", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "logo", + table: "movies", + type: "jsonb", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "poster", + table: "movies", + type: "jsonb", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "thumbnail", + table: "movies", + type: "jsonb", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "logo", + table: "episodes", + type: "jsonb", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "poster", + table: "episodes", + type: "jsonb", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "thumbnail", + table: "episodes", + type: "jsonb", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "logo", + table: "collections", + type: "jsonb", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "poster", + table: "collections", + type: "jsonb", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "thumbnail", + table: "collections", + type: "jsonb", + nullable: true + ); + + MigrateImage(migrationBuilder, "shows", "logo"); + MigrateImage(migrationBuilder, "shows", "poster"); + MigrateImage(migrationBuilder, "shows", "thumbnail"); + + MigrateImage(migrationBuilder, "seasons", "logo"); + MigrateImage(migrationBuilder, "seasons", "poster"); + MigrateImage(migrationBuilder, "seasons", "thumbnail"); + + MigrateImage(migrationBuilder, "movies", "logo"); + MigrateImage(migrationBuilder, "movies", "poster"); + MigrateImage(migrationBuilder, "movies", "thumbnail"); + + MigrateImage(migrationBuilder, "episodes", "logo"); + MigrateImage(migrationBuilder, "episodes", "poster"); + MigrateImage(migrationBuilder, "episodes", "thumbnail"); + + MigrateImage(migrationBuilder, "collections", "logo"); + MigrateImage(migrationBuilder, "collections", "poster"); + MigrateImage(migrationBuilder, "collections", "thumbnail"); + + migrationBuilder.DropColumn(name: "logo_blurhash", table: "shows"); + migrationBuilder.DropColumn(name: "logo_source", table: "shows"); + migrationBuilder.DropColumn(name: "poster_blurhash", table: "shows"); + migrationBuilder.DropColumn(name: "poster_source", table: "shows"); + migrationBuilder.DropColumn(name: "thumbnail_blurhash", table: "shows"); + migrationBuilder.DropColumn(name: "thumbnail_source", table: "shows"); + + migrationBuilder.DropColumn(name: "logo_blurhash", table: "seasons"); + migrationBuilder.DropColumn(name: "logo_source", table: "seasons"); + migrationBuilder.DropColumn(name: "poster_blurhash", table: "seasons"); + migrationBuilder.DropColumn(name: "poster_source", table: "seasons"); + migrationBuilder.DropColumn(name: "thumbnail_blurhash", table: "seasons"); + migrationBuilder.DropColumn(name: "thumbnail_source", table: "seasons"); + + migrationBuilder.DropColumn(name: "logo_blurhash", table: "movies"); + migrationBuilder.DropColumn(name: "logo_source", table: "movies"); + migrationBuilder.DropColumn(name: "poster_blurhash", table: "movies"); + migrationBuilder.DropColumn(name: "poster_source", table: "movies"); + migrationBuilder.DropColumn(name: "thumbnail_blurhash", table: "movies"); + migrationBuilder.DropColumn(name: "thumbnail_source", table: "movies"); + + migrationBuilder.DropColumn(name: "logo_blurhash", table: "episodes"); + migrationBuilder.DropColumn(name: "logo_source", table: "episodes"); + migrationBuilder.DropColumn(name: "poster_blurhash", table: "episodes"); + migrationBuilder.DropColumn(name: "poster_source", table: "episodes"); + migrationBuilder.DropColumn(name: "thumbnail_blurhash", table: "episodes"); + migrationBuilder.DropColumn(name: "thumbnail_source", table: "episodes"); + + migrationBuilder.DropColumn(name: "logo_blurhash", table: "collections"); + migrationBuilder.DropColumn(name: "logo_source", table: "collections"); + migrationBuilder.DropColumn(name: "poster_blurhash", table: "collections"); + migrationBuilder.DropColumn(name: "poster_source", table: "collections"); + migrationBuilder.DropColumn(name: "thumbnail_blurhash", table: "collections"); + migrationBuilder.DropColumn(name: "thumbnail_source", table: "collections"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "logo_blurhash", + table: "shows", + type: "character varying(32)", + maxLength: 32, + nullable: true + ); + + migrationBuilder.AddColumn( + name: "logo_source", + table: "shows", + type: "text", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "poster_blurhash", + table: "shows", + type: "character varying(32)", + maxLength: 32, + nullable: true + ); + + migrationBuilder.AddColumn( + name: "poster_source", + table: "shows", + type: "text", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "thumbnail_blurhash", + table: "shows", + type: "character varying(32)", + maxLength: 32, + nullable: true + ); + + migrationBuilder.AddColumn( + name: "thumbnail_source", + table: "shows", + type: "text", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "logo_blurhash", + table: "seasons", + type: "character varying(32)", + maxLength: 32, + nullable: true + ); + + migrationBuilder.AddColumn( + name: "logo_source", + table: "seasons", + type: "text", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "poster_blurhash", + table: "seasons", + type: "character varying(32)", + maxLength: 32, + nullable: true + ); + + migrationBuilder.AddColumn( + name: "poster_source", + table: "seasons", + type: "text", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "thumbnail_blurhash", + table: "seasons", + type: "character varying(32)", + maxLength: 32, + nullable: true + ); + + migrationBuilder.AddColumn( + name: "thumbnail_source", + table: "seasons", + type: "text", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "logo_blurhash", + table: "movies", + type: "character varying(32)", + maxLength: 32, + nullable: true + ); + + migrationBuilder.AddColumn( + name: "logo_source", + table: "movies", + type: "text", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "poster_blurhash", + table: "movies", + type: "character varying(32)", + maxLength: 32, + nullable: true + ); + + migrationBuilder.AddColumn( + name: "poster_source", + table: "movies", + type: "text", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "thumbnail_blurhash", + table: "movies", + type: "character varying(32)", + maxLength: 32, + nullable: true + ); + + migrationBuilder.AddColumn( + name: "thumbnail_source", + table: "movies", + type: "text", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "logo_blurhash", + table: "episodes", + type: "character varying(32)", + maxLength: 32, + nullable: true + ); + + migrationBuilder.AddColumn( + name: "logo_source", + table: "episodes", + type: "text", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "poster_blurhash", + table: "episodes", + type: "character varying(32)", + maxLength: 32, + nullable: true + ); + + migrationBuilder.AddColumn( + name: "poster_source", + table: "episodes", + type: "text", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "thumbnail_blurhash", + table: "episodes", + type: "character varying(32)", + maxLength: 32, + nullable: true + ); + + migrationBuilder.AddColumn( + name: "thumbnail_source", + table: "episodes", + type: "text", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "logo_blurhash", + table: "collections", + type: "character varying(32)", + maxLength: 32, + nullable: true + ); + + migrationBuilder.AddColumn( + name: "logo_source", + table: "collections", + type: "text", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "poster_blurhash", + table: "collections", + type: "character varying(32)", + maxLength: 32, + nullable: true + ); + + migrationBuilder.AddColumn( + name: "poster_source", + table: "collections", + type: "text", + nullable: true + ); + + migrationBuilder.AddColumn( + name: "thumbnail_blurhash", + table: "collections", + type: "character varying(32)", + maxLength: 32, + nullable: true + ); + + migrationBuilder.AddColumn( + name: "thumbnail_source", + table: "collections", + type: "text", + nullable: true + ); + + UnMigrateImage(migrationBuilder, "shows", "logo"); + UnMigrateImage(migrationBuilder, "shows", "poster"); + UnMigrateImage(migrationBuilder, "shows", "thumbnail"); + + UnMigrateImage(migrationBuilder, "seasons", "logo"); + UnMigrateImage(migrationBuilder, "seasons", "poster"); + UnMigrateImage(migrationBuilder, "seasons", "thumbnail"); + + UnMigrateImage(migrationBuilder, "movies", "logo"); + UnMigrateImage(migrationBuilder, "movies", "poster"); + UnMigrateImage(migrationBuilder, "movies", "thumbnail"); + + UnMigrateImage(migrationBuilder, "episodes", "logo"); + UnMigrateImage(migrationBuilder, "episodes", "poster"); + UnMigrateImage(migrationBuilder, "episodes", "thumbnail"); + + UnMigrateImage(migrationBuilder, "collections", "logo"); + UnMigrateImage(migrationBuilder, "collections", "poster"); + UnMigrateImage(migrationBuilder, "collections", "thumbnail"); + + migrationBuilder.DropColumn(name: "logo", table: "shows"); + migrationBuilder.DropColumn(name: "poster", table: "shows"); + migrationBuilder.DropColumn(name: "thumbnail", table: "shows"); + migrationBuilder.DropColumn(name: "logo", table: "seasons"); + migrationBuilder.DropColumn(name: "poster", table: "seasons"); + migrationBuilder.DropColumn(name: "thumbnail", table: "seasons"); + migrationBuilder.DropColumn(name: "logo", table: "movies"); + migrationBuilder.DropColumn(name: "poster", table: "movies"); + migrationBuilder.DropColumn(name: "thumbnail", table: "movies"); + migrationBuilder.DropColumn(name: "logo", table: "episodes"); + migrationBuilder.DropColumn(name: "poster", table: "episodes"); + migrationBuilder.DropColumn(name: "thumbnail", table: "episodes"); + migrationBuilder.DropColumn(name: "logo", table: "collections"); + migrationBuilder.DropColumn(name: "poster", table: "collections"); + migrationBuilder.DropColumn(name: "thumbnail", table: "collections"); + } + } +} diff --git a/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs b/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs index 8968aaca..f895138f 100644 --- a/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs +++ b/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs @@ -731,24 +731,26 @@ namespace Kyoo.Postgresql.Migrations b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => { b1.Property("CollectionId") - .HasColumnType("uuid") - .HasColumnName("id"); + .HasColumnType("uuid"); b1.Property("Blurhash") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("logo_blurhash"); + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); b1.Property("Source") .IsRequired() - .HasColumnType("text") - .HasColumnName("logo_source"); + .HasColumnType("text"); b1.HasKey("CollectionId"); b1.ToTable("collections"); + b1.ToJson("logo"); + b1.WithOwner() .HasForeignKey("CollectionId") .HasConstraintName("fk_collections_collections_id"); @@ -757,50 +759,55 @@ namespace Kyoo.Postgresql.Migrations b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => { b1.Property("CollectionId") - .HasColumnType("uuid") - .HasColumnName("id"); + .HasColumnType("uuid"); b1.Property("Blurhash") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("poster_blurhash"); + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); b1.Property("Source") .IsRequired() - .HasColumnType("text") - .HasColumnName("poster_source"); + .HasColumnType("text"); - b1.HasKey("CollectionId"); + b1.HasKey("CollectionId") + .HasName("pk_collections"); b1.ToTable("collections"); + b1.ToJson("poster"); + b1.WithOwner() .HasForeignKey("CollectionId") - .HasConstraintName("fk_collections_collections_id"); + .HasConstraintName("fk_collections_collections_collection_id"); }); b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => { b1.Property("CollectionId") - .HasColumnType("uuid") - .HasColumnName("id"); + .HasColumnType("uuid"); b1.Property("Blurhash") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("thumbnail_blurhash"); + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); b1.Property("Source") .IsRequired() - .HasColumnType("text") - .HasColumnName("thumbnail_source"); + .HasColumnType("text"); b1.HasKey("CollectionId"); b1.ToTable("collections"); + b1.ToJson("thumbnail"); + b1.WithOwner() .HasForeignKey("CollectionId") .HasConstraintName("fk_collections_collections_id"); @@ -831,24 +838,26 @@ namespace Kyoo.Postgresql.Migrations b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => { b1.Property("EpisodeId") - .HasColumnType("uuid") - .HasColumnName("id"); + .HasColumnType("uuid"); b1.Property("Blurhash") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("logo_blurhash"); + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); b1.Property("Source") .IsRequired() - .HasColumnType("text") - .HasColumnName("logo_source"); + .HasColumnType("text"); b1.HasKey("EpisodeId"); b1.ToTable("episodes"); + b1.ToJson("logo"); + b1.WithOwner() .HasForeignKey("EpisodeId") .HasConstraintName("fk_episodes_episodes_id"); @@ -857,24 +866,26 @@ namespace Kyoo.Postgresql.Migrations b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => { b1.Property("EpisodeId") - .HasColumnType("uuid") - .HasColumnName("id"); + .HasColumnType("uuid"); b1.Property("Blurhash") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("poster_blurhash"); + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); b1.Property("Source") .IsRequired() - .HasColumnType("text") - .HasColumnName("poster_source"); + .HasColumnType("text"); b1.HasKey("EpisodeId"); b1.ToTable("episodes"); + b1.ToJson("poster"); + b1.WithOwner() .HasForeignKey("EpisodeId") .HasConstraintName("fk_episodes_episodes_id"); @@ -883,24 +894,26 @@ namespace Kyoo.Postgresql.Migrations b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => { b1.Property("EpisodeId") - .HasColumnType("uuid") - .HasColumnName("id"); + .HasColumnType("uuid"); b1.Property("Blurhash") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("thumbnail_blurhash"); + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); b1.Property("Source") .IsRequired() - .HasColumnType("text") - .HasColumnName("thumbnail_source"); + .HasColumnType("text"); b1.HasKey("EpisodeId"); b1.ToTable("episodes"); + b1.ToJson("thumbnail"); + b1.WithOwner() .HasForeignKey("EpisodeId") .HasConstraintName("fk_episodes_episodes_id"); @@ -949,24 +962,26 @@ namespace Kyoo.Postgresql.Migrations b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => { b1.Property("MovieId") - .HasColumnType("uuid") - .HasColumnName("id"); + .HasColumnType("uuid"); b1.Property("Blurhash") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("logo_blurhash"); + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); b1.Property("Source") .IsRequired() - .HasColumnType("text") - .HasColumnName("logo_source"); + .HasColumnType("text"); b1.HasKey("MovieId"); b1.ToTable("movies"); + b1.ToJson("logo"); + b1.WithOwner() .HasForeignKey("MovieId") .HasConstraintName("fk_movies_movies_id"); @@ -975,24 +990,26 @@ namespace Kyoo.Postgresql.Migrations b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => { b1.Property("MovieId") - .HasColumnType("uuid") - .HasColumnName("id"); + .HasColumnType("uuid"); b1.Property("Blurhash") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("poster_blurhash"); + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); b1.Property("Source") .IsRequired() - .HasColumnType("text") - .HasColumnName("poster_source"); + .HasColumnType("text"); b1.HasKey("MovieId"); b1.ToTable("movies"); + b1.ToJson("poster"); + b1.WithOwner() .HasForeignKey("MovieId") .HasConstraintName("fk_movies_movies_id"); @@ -1001,24 +1018,26 @@ namespace Kyoo.Postgresql.Migrations b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => { b1.Property("MovieId") - .HasColumnType("uuid") - .HasColumnName("id"); + .HasColumnType("uuid"); b1.Property("Blurhash") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("thumbnail_blurhash"); + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); b1.Property("Source") .IsRequired() - .HasColumnType("text") - .HasColumnName("thumbnail_source"); + .HasColumnType("text"); b1.HasKey("MovieId"); b1.ToTable("movies"); + b1.ToJson("thumbnail"); + b1.WithOwner() .HasForeignKey("MovieId") .HasConstraintName("fk_movies_movies_id"); @@ -1066,24 +1085,26 @@ namespace Kyoo.Postgresql.Migrations b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => { b1.Property("SeasonId") - .HasColumnType("uuid") - .HasColumnName("id"); + .HasColumnType("uuid"); b1.Property("Blurhash") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("logo_blurhash"); + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); b1.Property("Source") .IsRequired() - .HasColumnType("text") - .HasColumnName("logo_source"); + .HasColumnType("text"); b1.HasKey("SeasonId"); b1.ToTable("seasons"); + b1.ToJson("logo"); + b1.WithOwner() .HasForeignKey("SeasonId") .HasConstraintName("fk_seasons_seasons_id"); @@ -1092,24 +1113,26 @@ namespace Kyoo.Postgresql.Migrations b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => { b1.Property("SeasonId") - .HasColumnType("uuid") - .HasColumnName("id"); + .HasColumnType("uuid"); b1.Property("Blurhash") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("poster_blurhash"); + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); b1.Property("Source") .IsRequired() - .HasColumnType("text") - .HasColumnName("poster_source"); + .HasColumnType("text"); b1.HasKey("SeasonId"); b1.ToTable("seasons"); + b1.ToJson("poster"); + b1.WithOwner() .HasForeignKey("SeasonId") .HasConstraintName("fk_seasons_seasons_id"); @@ -1118,24 +1141,26 @@ namespace Kyoo.Postgresql.Migrations b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => { b1.Property("SeasonId") - .HasColumnType("uuid") - .HasColumnName("id"); + .HasColumnType("uuid"); b1.Property("Blurhash") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("thumbnail_blurhash"); + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); b1.Property("Source") .IsRequired() - .HasColumnType("text") - .HasColumnName("thumbnail_source"); + .HasColumnType("text"); b1.HasKey("SeasonId"); b1.ToTable("seasons"); + b1.ToJson("thumbnail"); + b1.WithOwner() .HasForeignKey("SeasonId") .HasConstraintName("fk_seasons_seasons_id"); @@ -1161,24 +1186,26 @@ namespace Kyoo.Postgresql.Migrations b.OwnsOne("Kyoo.Abstractions.Models.Image", "Logo", b1 => { b1.Property("ShowId") - .HasColumnType("uuid") - .HasColumnName("id"); + .HasColumnType("uuid"); b1.Property("Blurhash") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("logo_blurhash"); + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); b1.Property("Source") .IsRequired() - .HasColumnType("text") - .HasColumnName("logo_source"); + .HasColumnType("text"); b1.HasKey("ShowId"); b1.ToTable("shows"); + b1.ToJson("logo"); + b1.WithOwner() .HasForeignKey("ShowId") .HasConstraintName("fk_shows_shows_id"); @@ -1187,24 +1214,26 @@ namespace Kyoo.Postgresql.Migrations b.OwnsOne("Kyoo.Abstractions.Models.Image", "Poster", b1 => { b1.Property("ShowId") - .HasColumnType("uuid") - .HasColumnName("id"); + .HasColumnType("uuid"); b1.Property("Blurhash") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("poster_blurhash"); + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); b1.Property("Source") .IsRequired() - .HasColumnType("text") - .HasColumnName("poster_source"); + .HasColumnType("text"); b1.HasKey("ShowId"); b1.ToTable("shows"); + b1.ToJson("poster"); + b1.WithOwner() .HasForeignKey("ShowId") .HasConstraintName("fk_shows_shows_id"); @@ -1213,24 +1242,26 @@ namespace Kyoo.Postgresql.Migrations b.OwnsOne("Kyoo.Abstractions.Models.Image", "Thumbnail", b1 => { b1.Property("ShowId") - .HasColumnType("uuid") - .HasColumnName("id"); + .HasColumnType("uuid"); b1.Property("Blurhash") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("thumbnail_blurhash"); + .HasColumnType("character varying(32)"); + + b1.Property("Id") + .HasColumnType("uuid"); b1.Property("Source") .IsRequired() - .HasColumnType("text") - .HasColumnName("thumbnail_source"); + .HasColumnType("text"); b1.HasKey("ShowId"); b1.ToTable("shows"); + b1.ToJson("thumbnail"); + b1.WithOwner() .HasForeignKey("ShowId") .HasConstraintName("fk_shows_shows_id"); From bfbc66cdc0a3a667533a3ee554daeb8d5568f7a5 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 02:04:56 +0200 Subject: [PATCH 12/26] Add env var for rabbitmq port --- .env.example | 1 + autosync/autosync/__init__.py | 1 + autosync/autosync/services/simkl.py | 4 ++-- back/src/Kyoo.RabbitMq/RabbitMqModule.cs | 2 +- scanner/matcher/subscriber.py | 7 +++++++ scanner/scanner/publisher.py | 1 + 6 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index c70dc9fc..30ffddba 100644 --- a/.env.example +++ b/.env.example @@ -75,5 +75,6 @@ MEILI_HOST="http://meilisearch:7700" MEILI_MASTER_KEY="ghvjkgisbgkbgskegblfqbgjkebbhgwkjfb" RABBITMQ_HOST=rabbitmq +RABBITMQ_PORT=5672 RABBITMQ_DEFAULT_USER=kyoo RABBITMQ_DEFAULT_PASS=aohohunuhouhuhhoahothonseuhaoensuthoaentsuhha diff --git a/autosync/autosync/__init__.py b/autosync/autosync/__init__.py index 8b1eda73..58727949 100644 --- a/autosync/autosync/__init__.py +++ b/autosync/autosync/__init__.py @@ -46,6 +46,7 @@ def main(): connection = pika.BlockingConnection( pika.ConnectionParameters( host=os.environ.get("RABBITMQ_HOST", "rabbitmq"), + port=os.environ.get("RABBITMQ_PORT", 5672), credentials=pika.credentials.PlainCredentials( os.environ.get("RABBITMQ_DEFAULT_USER", "guest"), os.environ.get("RABBITMQ_DEFAULT_PASS", "guest"), diff --git a/autosync/autosync/services/simkl.py b/autosync/autosync/services/simkl.py index c78319d9..74df8cbc 100644 --- a/autosync/autosync/services/simkl.py +++ b/autosync/autosync/services/simkl.py @@ -56,7 +56,7 @@ class Simkl(Service): ] }, headers={ - "Authorization": f"Bearer {user.external_id["simkl"].token.access_token}", + "Authorization": f"Bearer {user.external_id['simkl'].token.access_token}", "simkl-api-key": self._api_key, }, ) @@ -85,7 +85,7 @@ class Simkl(Service): ] }, headers={ - "Authorization": f"Bearer {user.external_id["simkl"].token.access_token}", + "Authorization": f"Bearer {user.external_id['simkl'].token.access_token}", "simkl-api-key": self._api_key, }, ) diff --git a/back/src/Kyoo.RabbitMq/RabbitMqModule.cs b/back/src/Kyoo.RabbitMq/RabbitMqModule.cs index 86b7b3d8..aba89bb2 100644 --- a/back/src/Kyoo.RabbitMq/RabbitMqModule.cs +++ b/back/src/Kyoo.RabbitMq/RabbitMqModule.cs @@ -35,7 +35,7 @@ public static class RabbitMqModule UserName = builder.Configuration.GetValue("RABBITMQ_DEFAULT_USER", "guest"), Password = builder.Configuration.GetValue("RABBITMQ_DEFAULT_PASS", "guest"), HostName = builder.Configuration.GetValue("RABBITMQ_HOST", "rabbitmq"), - Port = 5672, + Port = builder.Configuration.GetValue("RABBITMQ_Port", 5672), }; return factory.CreateConnection(); diff --git a/scanner/matcher/subscriber.py b/scanner/matcher/subscriber.py index 728cf5bd..ca2fc603 100644 --- a/scanner/matcher/subscriber.py +++ b/scanner/matcher/subscriber.py @@ -10,27 +10,34 @@ from matcher.matcher import Matcher logger = logging.getLogger(__name__) + class Message(Struct, tag_field="action", tag=str.lower): pass + class Scan(Message): path: str + class Delete(Message): path: str + class Refresh(Message): kind: Literal["collection", "show", "movie", "season", "episode"] id: str + decoder = json.Decoder(Union[Scan, Delete, Refresh]) + class Subscriber: QUEUE = "scanner" async def __aenter__(self): self._con = await connect_robust( host=os.environ.get("RABBITMQ_HOST", "rabbitmq"), + port=int(os.environ.get("RABBITMQ_PORT", "5672")), login=os.environ.get("RABBITMQ_DEFAULT_USER", "guest"), password=os.environ.get("RABBITMQ_DEFAULT_PASS", "guest"), ) diff --git a/scanner/scanner/publisher.py b/scanner/scanner/publisher.py index 7a99295d..2c4e1838 100644 --- a/scanner/scanner/publisher.py +++ b/scanner/scanner/publisher.py @@ -9,6 +9,7 @@ class Publisher: async def __aenter__(self): self._con = await connect_robust( host=os.environ.get("RABBITMQ_HOST", "rabbitmq"), + port=int(os.environ.get("RABBITMQ_PORT", "5672")), login=os.environ.get("RABBITMQ_DEFAULT_USER", "guest"), password=os.environ.get("RABBITMQ_DEFAULT_PASS", "guest"), ) From ac3b593b8b47d5707089c276e834eda0fed99811 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 02:18:10 +0200 Subject: [PATCH 13/26] Add ignore permissions errors on watchfile --- scanner/scanner/monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanner/scanner/monitor.py b/scanner/scanner/monitor.py index 1fe24ace..439bc93f 100644 --- a/scanner/scanner/monitor.py +++ b/scanner/scanner/monitor.py @@ -7,7 +7,7 @@ logger = getLogger(__name__) async def monitor(path: str, publisher: Publisher): - async for changes in awatch(path): + async for changes in awatch(path, ignore_permission_denied=True): for event, file in changes: if event == Change.added: await publisher.add(file) From d872e66f7d97eaa1057efcc915bec1af2c1ffac4 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 02:32:09 +0200 Subject: [PATCH 14/26] Fix metadat volume in docker compose --- docker-compose.build.yml | 2 +- docker-compose.dev.yml | 2 +- docker-compose.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.build.yml b/docker-compose.build.yml index da339068..dd98809b 100644 --- a/docker-compose.build.yml +++ b/docker-compose.build.yml @@ -30,7 +30,7 @@ services: migrations: condition: service_completed_successfully volumes: - - kyoo:/kyoo + - kyoo:/metadata migrations: build: diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 78199a5c..4ee3f10a 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -42,7 +42,7 @@ services: volumes: - ./back:/app - /app/out/ - - kyoo:/kyoo + - kyoo:/metadata migrations: build: diff --git a/docker-compose.yml b/docker-compose.yml index ac7843ae..036b295e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,7 @@ services: migrations: condition: service_completed_successfully volumes: - - kyoo:/kyoo + - kyoo:/metadata migrations: image: zoriya/kyoo_migrations:latest From b904f25d33b76bf1fd57199cff1aee421160b8a4 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 13:41:25 +0200 Subject: [PATCH 15/26] Remove image move code --- back/src/Kyoo.Core/Program.cs | 37 ----------------------------------- 1 file changed, 37 deletions(-) diff --git a/back/src/Kyoo.Core/Program.cs b/back/src/Kyoo.Core/Program.cs index bebd1c2b..83bbe127 100644 --- a/back/src/Kyoo.Core/Program.cs +++ b/back/src/Kyoo.Core/Program.cs @@ -17,7 +17,6 @@ // along with Kyoo. If not, see . using System; -using System.IO; using Kyoo.Authentication; using Kyoo.Core; using Kyoo.Core.Extensions; @@ -93,42 +92,6 @@ app.UseRouting(); app.UseAuthentication(); app.MapControllers(); -// TODO: wait 4.5.0 and delete this -static void MoveAll(DirectoryInfo source, DirectoryInfo target) -{ - if (source.FullName == target.FullName) - return; - - Directory.CreateDirectory(target.FullName); - - foreach (FileInfo fi in source.GetFiles()) - fi.MoveTo(Path.Combine(target.ToString(), fi.Name), true); - - foreach (DirectoryInfo diSourceSubDir in source.GetDirectories()) - { - DirectoryInfo nextTargetSubDir = target.CreateSubdirectory(diSourceSubDir.Name); - MoveAll(diSourceSubDir, nextTargetSubDir); - } - Directory.Delete(source.FullName); -} - -try -{ - string oldDir = "/kyoo/kyoo_datadir/metadata"; - if (Path.Exists(oldDir)) - { - MoveAll(new DirectoryInfo(oldDir), new DirectoryInfo("/metadata")); - Log.Warning("Old metadata directory migrated."); - } -} -catch (Exception ex) -{ - Log.Fatal( - ex, - "Unhandled error while trying to migrate old metadata images to new directory. Giving up and continuing normal startup." - ); -} - // Activate services that always run in the background app.Services.GetRequiredService(); app.Services.GetRequiredService(); From e9b29dd814ddcd24602d02f242e477322df980c5 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 13:41:48 +0200 Subject: [PATCH 16/26] Add image creation code in Validate() of resources --- .../Repositories/CollectionRepository.cs | 5 ++- .../Repositories/EpisodeRepository.cs | 3 +- .../Repositories/MovieRepository.cs | 8 +++-- .../Repositories/SeasonRepository.cs | 4 ++- .../Repositories/ShowRepository.cs | 8 +++-- .../Controllers/ThumbnailsManager.cs | 33 +++++++++---------- 6 files changed, 36 insertions(+), 25 deletions(-) diff --git a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs index 9a7682ef..d9f8499b 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs @@ -20,6 +20,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Utils; using Kyoo.Postgresql; @@ -30,7 +31,8 @@ namespace Kyoo.Core.Controllers; /// /// A local repository to handle collections /// -public class CollectionRepository(DatabaseContext database) : GenericRepository(database) +public class CollectionRepository(DatabaseContext database, IThumbnailsManager thumbnails) + : GenericRepository(database) { /// public override async Task> Search( @@ -51,6 +53,7 @@ public class CollectionRepository(DatabaseContext database) : GenericRepository< if (string.IsNullOrEmpty(resource.Name)) throw new ArgumentException("The collection's name must be set and not empty"); + await thumbnails.DownloadImages(resource); } public async Task AddMovie(Guid id, Guid movieId) diff --git a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs index ff523866..30e8a7a9 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs @@ -32,7 +32,7 @@ namespace Kyoo.Core.Controllers; /// /// A local repository to handle episodes. /// -public class EpisodeRepository(DatabaseContext database, IRepository shows) +public class EpisodeRepository(DatabaseContext database, IRepository shows, IThumbnailsManager thumbnails) : GenericRepository(database) { static EpisodeRepository() @@ -96,6 +96,7 @@ public class EpisodeRepository(DatabaseContext database, IRepository shows x.ShowId == resource.ShowId && x.SeasonNumber == resource.SeasonNumber ); } + await thumbnails.DownloadImages(resource); } /// diff --git a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs index 569e6274..25d5792c 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs @@ -27,8 +27,11 @@ using Microsoft.EntityFrameworkCore; namespace Kyoo.Core.Controllers; -public class MovieRepository(DatabaseContext database, IRepository studios) - : GenericRepository(database) +public class MovieRepository( + DatabaseContext database, + IRepository studios, + IThumbnailsManager thumbnails +) : GenericRepository(database) { /// public override async Task> Search( @@ -51,5 +54,6 @@ public class MovieRepository(DatabaseContext database, IRepository studi resource.Studio = await studios.CreateIfNotExists(resource.Studio); resource.StudioId = resource.Studio.Id; } + await thumbnails.DownloadImages(resource); } } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs index 18d9d47c..9d7b46bb 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs @@ -31,7 +31,8 @@ using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Core.Controllers; -public class SeasonRepository(DatabaseContext database) : GenericRepository(database) +public class SeasonRepository(DatabaseContext database, IThumbnailsManager thumbnails) + : GenericRepository(database) { static SeasonRepository() { @@ -90,5 +91,6 @@ public class SeasonRepository(DatabaseContext database) : GenericRepository studios) - : GenericRepository(database) +public class ShowRepository( + DatabaseContext database, + IRepository studios, + IThumbnailsManager thumbnails +) : GenericRepository(database) { /// public override async Task> Search( @@ -51,5 +54,6 @@ public class ShowRepository(DatabaseContext database, IRepository studio resource.Studio = await studios.CreateIfNotExists(resource.Studio); resource.StudioId = resource.Studio.Id; } + await thumbnails.DownloadImages(resource); } } diff --git a/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs b/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs index 0ec1d154..d896127c 100644 --- a/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs +++ b/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs @@ -57,8 +57,17 @@ public class ThumbnailsManager( try { if (image.Id == Guid.Empty) - image.Id = new Guid(); - string localPath = $"/metadata/{image.Id}"; + { + using MD5 md5 = MD5.Create(); + image.Id = new Guid(md5.ComputeHash(Encoding.UTF8.GetBytes(image.Source))); + } + + if ( + File.Exists(GetImagePath(image.Id, ImageQuality.High)) + && File.Exists(GetImagePath(image.Id, ImageQuality.Medium)) + && File.Exists(GetImagePath(image.Id, ImageQuality.Low)) + ) + return; logger.LogInformation("Downloading image {What}", what); @@ -81,31 +90,19 @@ public class ThumbnailsManager( new SKSizeI(original.Width, original.Height), SKFilterQuality.High ); - await _WriteTo( - original, - $"{localPath}.{ImageQuality.High.ToString().ToLowerInvariant()}.webp", - 90 - ); + await _WriteTo(original, GetImagePath(image.Id, ImageQuality.High), 90); using SKBitmap medium = high.Resize( new SKSizeI((int)(high.Width / 1.5), (int)(high.Height / 1.5)), SKFilterQuality.Medium ); - await _WriteTo( - medium, - $"{localPath}.{ImageQuality.Medium.ToString().ToLowerInvariant()}.webp", - 75 - ); + await _WriteTo(medium, GetImagePath(image.Id, ImageQuality.Medium), 75); using SKBitmap low = medium.Resize( new SKSizeI(original.Width / 2, original.Height / 2), SKFilterQuality.Low ); - await _WriteTo( - low, - $"{localPath}.{ImageQuality.Low.ToString().ToLowerInvariant()}.webp", - 50 - ); + await _WriteTo(low, GetImagePath(image.Id, ImageQuality.Low), 50); image.Blurhash = Blurhasher.Encode(low, 4, 3); } @@ -136,7 +133,7 @@ public class ThumbnailsManager( public Task DeleteImages(T item) where T : IThumbnails { - IEnumerable images = new[] {item.Poster?.Id, item.Thumbnail?.Id, item.Logo?.Id} + IEnumerable images = new[] { item.Poster?.Id, item.Thumbnail?.Id, item.Logo?.Id } .Where(x => x is not null) .SelectMany(x => $"/metadata/{x}") .SelectMany(x => From 8b256e4c60955754d801821aba4d8fe201129821 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 13:52:13 +0200 Subject: [PATCH 17/26] Remove image hash caching since it does not work for blurhash --- back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs b/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs index d896127c..6a8d28a3 100644 --- a/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs +++ b/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs @@ -57,17 +57,7 @@ public class ThumbnailsManager( try { if (image.Id == Guid.Empty) - { - using MD5 md5 = MD5.Create(); - image.Id = new Guid(md5.ComputeHash(Encoding.UTF8.GetBytes(image.Source))); - } - - if ( - File.Exists(GetImagePath(image.Id, ImageQuality.High)) - && File.Exists(GetImagePath(image.Id, ImageQuality.Medium)) - && File.Exists(GetImagePath(image.Id, ImageQuality.Low)) - ) - return; + image.Id = Guid.NewGuid(); logger.LogInformation("Downloading image {What}", what); From d36a20ce5eb920329f8ac618d1cedaaed2ad5486 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 14:02:22 +0200 Subject: [PATCH 18/26] Move different year duplication prevention logic to the API --- .../Repositories/MovieRepository.cs | 20 +++++++++++++++++++ .../Repositories/ShowRepository.cs | 20 +++++++++++++++++++ scanner/providers/kyoo_client.py | 14 ------------- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs index 25d5792c..4ab8b73a 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs @@ -21,6 +21,7 @@ using System.Linq; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; +using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Utils; using Kyoo.Postgresql; using Microsoft.EntityFrameworkCore; @@ -45,6 +46,25 @@ public class MovieRepository( .ToListAsync(); } + /// + public override Task Create(Movie obj) + { + try + { + return base.Create(obj); + } + catch (DuplicatedItemException ex) + when (ex.Existing is Movie existing + && existing.Slug == obj.Slug + && obj.AirDate is not null + && existing.AirDate?.Year != obj.AirDate?.Year + ) + { + obj.Slug = $"{obj.Slug}-{obj.AirDate!.Value.Year}"; + return base.Create(obj); + } + } + /// protected override async Task Validate(Movie resource) { diff --git a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs index 53a3b6d5..ed67e2df 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs @@ -21,6 +21,7 @@ using System.Linq; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; +using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Utils; using Kyoo.Postgresql; using Microsoft.EntityFrameworkCore; @@ -45,6 +46,25 @@ public class ShowRepository( .ToListAsync(); } + /// + public override Task Create(Show obj) + { + try + { + return base.Create(obj); + } + catch (DuplicatedItemException ex) + when (ex.Existing is Show existing + && existing.Slug == obj.Slug + && obj.StartAir is not null + && existing.StartAir?.Year != obj.StartAir?.Year + ) + { + obj.Slug = $"{obj.Slug}-{obj.StartAir!.Value.Year}"; + return base.Create(obj); + } + } + /// protected override async Task Validate(Show resource) { diff --git a/scanner/providers/kyoo_client.py b/scanner/providers/kyoo_client.py index b804b740..47a16c93 100644 --- a/scanner/providers/kyoo_client.py +++ b/scanner/providers/kyoo_client.py @@ -112,20 +112,6 @@ class KyooClient: logger.error(f"Request error: {await r.text()}") r.raise_for_status() ret = await r.json() - - if r.status == 409 and ( - (path == "shows" and ret["startAir"][:4] != str(data["start_air"].year)) - or ( - path == "movies" - and ret["airDate"][:4] != str(data["air_date"].year) - ) - ): - logger.info( - f"Found a {path} with the same slug ({ret['slug']}) and a different date, using the date as part of the slug" - ) - year = (data["start_air"] if path == "movie" else data["air_date"]).year - data["slug"] = f"{ret['slug']}-{year}" - return await self.post(path, data=data) return ret["id"] async def delete( From e846a1cc18146071585b4667f5b23a9eaf4f3cc1 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 14:12:06 +0200 Subject: [PATCH 19/26] Remove dead code --- .../Utility/EnumerableExtensions.cs | 70 -------- back/src/Kyoo.Abstractions/Utility/Merger.cs | 133 --------------- back/src/Kyoo.Abstractions/Utility/Utility.cs | 160 ------------------ 3 files changed, 363 deletions(-) delete mode 100644 back/src/Kyoo.Abstractions/Utility/EnumerableExtensions.cs delete mode 100644 back/src/Kyoo.Abstractions/Utility/Merger.cs diff --git a/back/src/Kyoo.Abstractions/Utility/EnumerableExtensions.cs b/back/src/Kyoo.Abstractions/Utility/EnumerableExtensions.cs deleted file mode 100644 index 4c3b72b0..00000000 --- a/back/src/Kyoo.Abstractions/Utility/EnumerableExtensions.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System; -using System.Collections.Generic; - -namespace Kyoo.Utils; - -/// -/// A set of extensions class for enumerable. -/// -public static class EnumerableExtensions -{ - /// - /// If the enumerable is empty, execute an action. - /// - /// The enumerable to check - /// The action to execute is the list is empty - /// The type of items inside the list - /// The iterator proxied, there is no dual iterations. - public static IEnumerable IfEmpty(this IEnumerable self, Action action) - { - static IEnumerable Generator(IEnumerable self, Action action) - { - using IEnumerator enumerator = self.GetEnumerator(); - - if (!enumerator.MoveNext()) - { - action(); - yield break; - } - - do - { - yield return enumerator.Current; - } while (enumerator.MoveNext()); - } - - return Generator(self, action); - } - - /// - /// A foreach used as a function with a little specificity: the list can be null. - /// - /// The list to enumerate. If this is null, the function result in a no-op - /// The action to execute for each arguments - /// The type of items in the list - public static void ForEach(this IEnumerable? self, Action action) - { - if (self == null) - return; - foreach (T i in self) - action(i); - } -} diff --git a/back/src/Kyoo.Abstractions/Utility/Merger.cs b/back/src/Kyoo.Abstractions/Utility/Merger.cs deleted file mode 100644 index a97530ef..00000000 --- a/back/src/Kyoo.Abstractions/Utility/Merger.cs +++ /dev/null @@ -1,133 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Kyoo.Abstractions.Models.Attributes; - -namespace Kyoo.Utils; - -/// -/// A class containing helper methods to merge objects. -/// -public static class Merger -{ - /// - /// Merge two dictionary, if the same key is found on both dictionary, the values of the second one is kept. - /// - /// The first dictionary to merge - /// The second dictionary to merge - /// - /// true if a new items has been added to the dictionary, false otherwise. - /// - /// The type of the keys in dictionaries - /// The type of values in the dictionaries - /// - /// A dictionary with the missing elements of - /// set to those of . - /// - public static IDictionary? CompleteDictionaries( - IDictionary? first, - IDictionary? second, - out bool hasChanged - ) - { - if (first == null) - { - hasChanged = true; - return second; - } - - hasChanged = false; - if (second == null) - return first; - hasChanged = second.Any(x => - !first.ContainsKey(x.Key) || x.Value?.Equals(first[x.Key]) == false - ); - foreach ((T key, T2 value) in first) - second.TryAdd(key, value); - return second; - } - - /// - /// Set every non-default values of seconds to the corresponding property of second. - /// Dictionaries are handled like anonymous objects with a property per key/pair value - /// At the end, the OnMerge method of first will be called if first is a - /// - /// - /// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "foo"} - /// - /// - /// The object to complete - /// - /// - /// Missing fields of first will be completed by fields of this item. If second is null, the function no-op. - /// - /// - /// Filter fields that will be merged - /// - /// Fields of T will be completed - /// - public static T Complete(T first, T? second, Func? where = null) - { - if (second == null) - return first; - - Type type = typeof(T); - IEnumerable properties = type.GetProperties() - .Where(x => - x is { CanRead: true, CanWrite: true } - && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null - ); - - if (where != null) - properties = properties.Where(where); - - foreach (PropertyInfo property in properties) - { - object? value = property.GetValue(second); - - if (value?.Equals(property.GetValue(first)) == true) - continue; - - if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>))) - { - Type[] dictionaryTypes = Utility - .GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))! - .GenericTypeArguments; - object?[] parameters = { property.GetValue(first), value, false }; - object newDictionary = Utility.RunGenericMethod( - typeof(Merger), - nameof(CompleteDictionaries), - dictionaryTypes, - parameters - )!; - if ((bool)parameters[2]!) - property.SetValue(first, newDictionary); - } - else - property.SetValue(first, value); - } - - if (first is IOnMerge merge) - merge.OnMerge(second); - return first; - } -} diff --git a/back/src/Kyoo.Abstractions/Utility/Utility.cs b/back/src/Kyoo.Abstractions/Utility/Utility.cs index 422d673d..9e085457 100644 --- a/back/src/Kyoo.Abstractions/Utility/Utility.cs +++ b/back/src/Kyoo.Abstractions/Utility/Utility.cs @@ -177,25 +177,6 @@ public static class Utility yield return type; } - /// - /// Check if inherit from a generic type . - /// - /// The type to check - /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). - /// True if obj inherit from genericType. False otherwise - public static bool IsOfGenericType(Type type, Type genericType) - { - if (!genericType.IsGenericType) - throw new ArgumentException($"{nameof(genericType)} is not a generic type."); - - IEnumerable types = genericType.IsInterface - ? type.GetInterfaces() - : type.GetInheritanceTree(); - return types - .Prepend(type) - .Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); - } - /// /// Get the generic definition of . /// For example, calling this function with List<string> and typeof(IEnumerable<>) will return IEnumerable<string> @@ -217,147 +198,6 @@ public static class Utility .FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); } - /// - /// Retrieve a method from an with the given name and respect the - /// amount of parameters and generic parameters. This works for polymorphic methods. - /// - /// - /// The type owning the method. For non static methods, this is the this. - /// - /// - /// The binding flags of the method. This allow you to specify public/private and so on. - /// - /// - /// The name of the method. - /// - /// - /// The list of generic parameters. - /// - /// - /// The list of parameters. - /// - /// No method match the given constraints. - /// The method handle of the matching method. - public static MethodInfo GetMethod( - Type type, - BindingFlags flag, - string name, - Type[] generics, - object?[] args - ) - { - MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public) - .Where(x => x.Name == name) - .Where(x => x.GetGenericArguments().Length == generics.Length) - .Where(x => x.GetParameters().Length == args.Length) - .IfEmpty(() => - { - throw new ArgumentException( - $"A method named {name} with " - + $"{args.Length} arguments and {generics.Length} generic " - + $"types could not be found on {type.Name}." - ); - }) - // TODO this won't work but I don't know why. - // .Where(x => - // { - // int i = 0; - // return x.GetGenericArguments().All(y => y.IsAssignableFrom(generics[i++])); - // }) - // .IfEmpty(() => throw new NullReferenceException($"No method {name} match the generics specified.")) - - // TODO this won't work for Type because T is specified in arguments but not in the parameters type. - // .Where(x => - // { - // int i = 0; - // return x.GetParameters().All(y => y.ParameterType.IsInstanceOfType(args[i++])); - // }) - // .IfEmpty(() => throw new NullReferenceException($"No method {name} match the parameters's types.")) - .Take(2) - .ToArray(); - - if (methods.Length == 1) - return methods[0]; - throw new ArgumentException( - $"Multiple methods named {name} match the generics and parameters constraints." - ); - } - - /// - /// Run a generic static method for a runtime . - /// - /// - /// To run Merger.MergeLists{T} for a List where you don't know the type at compile type, - /// you could do: - /// - /// Utility.RunGenericMethod<object>( - /// typeof(Utility), - /// nameof(MergeLists), - /// enumerableType, - /// oldValue, newValue, equalityComparer) - /// - /// - /// The type that owns the method. For non static methods, the type of this. - /// The name of the method. You should use the nameof keyword. - /// The generic type to run the method with. - /// The list of arguments of the method - /// - /// The return type of the method. You can put for an unknown one. - /// - /// No method match the given constraints. - /// The return of the method you wanted to run. - /// - public static T? RunGenericMethod( - Type owner, - string methodName, - Type type, - params object[] args - ) - { - return RunGenericMethod(owner, methodName, new[] { type }, args); - } - - /// - /// Run a generic static method for a multiple runtime . - /// If your generic method only needs one type, see - /// - /// - /// - /// To run Merger.MergeLists{T} for a List where you don't know the type at compile type, - /// you could do: - /// - /// Utility.RunGenericMethod<object>( - /// typeof(Utility), - /// nameof(MergeLists), - /// enumerableType, - /// oldValue, newValue, equalityComparer) - /// - /// - /// The type that owns the method. For non static methods, the type of this. - /// The name of the method. You should use the nameof keyword. - /// The list of generic types to run the method with. - /// The list of arguments of the method - /// - /// The return type of the method. You can put for an unknown one. - /// - /// No method match the given constraints. - /// The return of the method you wanted to run. - /// - public static T? RunGenericMethod( - Type owner, - string methodName, - Type[] types, - params object?[] args - ) - { - if (types.Length < 1) - throw new ArgumentException( - $"The {nameof(types)} array is empty. At least one type is needed." - ); - MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args); - return (T?)method.MakeGenericMethod(types).Invoke(null, args); - } - /// /// Convert a dictionary to a query string. /// From 00cd94d624a0f76796687f5c5c1165358e9580ea Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 19:39:40 +0200 Subject: [PATCH 20/26] Fix repository's relations handling --- .../Repositories/EpisodeRepository.cs | 18 ++++++------------ .../Repositories/GenericRepository.cs | 11 ++++++----- .../Repositories/LibraryItemRepository.cs | 6 ++---- .../Repositories/MovieRepository.cs | 4 ++-- .../Repositories/SeasonRepository.cs | 12 ++---------- .../Controllers/Repositories/ShowRepository.cs | 6 +++--- 6 files changed, 21 insertions(+), 36 deletions(-) diff --git a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs index 30e8a7a9..3820102c 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; @@ -79,22 +80,15 @@ public class EpisodeRepository(DatabaseContext database, IRepository shows protected override async Task Validate(Episode resource) { await base.Validate(resource); + resource.Show = null; if (resource.ShowId == Guid.Empty) - { - if (resource.Show == null) - { - throw new ArgumentException( - $"Can't store an episode not related " - + $"to any show (showID: {resource.ShowId})." - ); - } - resource.ShowId = resource.Show.Id; - } + throw new ValidationException("Missing show id"); + resource.Season = null; if (resource.SeasonId == null && resource.SeasonNumber != null) { - resource.Season = await Database.Seasons.FirstOrDefaultAsync(x => + resource.SeasonId = await Database.Seasons.Where(x => x.ShowId == resource.ShowId && x.SeasonNumber == resource.SeasonNumber - ); + ).Select(x => x.Id).FirstOrDefaultAsync(); } await thumbnails.DownloadImages(resource); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/GenericRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/GenericRepository.cs index a862aabd..dbe199f2 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/GenericRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/GenericRepository.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -268,7 +269,7 @@ public abstract class GenericRepository(DatabaseContext database) : IReposito public virtual async Task Create(T obj) { await Validate(obj); - Database.Entry(obj).State = EntityState.Added; + Database.Add(obj); await Database.SaveChangesAsync(() => Get(obj.Slug)); await IRepository.OnResourceCreated(obj); return obj; @@ -295,7 +296,7 @@ public abstract class GenericRepository(DatabaseContext database) : IReposito public virtual async Task Edit(T edited) { await Validate(edited); - Database.Entry(edited).State = EntityState.Modified; + Database.Update(edited); await Database.SaveChangesAsync(); await IRepository.OnResourceEdited(edited); return edited; @@ -323,7 +324,7 @@ public abstract class GenericRepository(DatabaseContext database) : IReposito } } - /// + /// /// You can throw this if the resource is illegal and should not be saved. /// protected virtual Task Validate(T resource) @@ -334,9 +335,9 @@ public abstract class GenericRepository(DatabaseContext database) : IReposito ) return Task.CompletedTask; if (string.IsNullOrEmpty(resource.Slug)) - throw new ArgumentException("Resource can't have null as a slug."); + throw new ValidationException("Resource can't have null as a slug."); if (resource.Slug == "random") - throw new ArgumentException("Resources slug can't be the literal \"random\"."); + throw new ValidationException("Resources slug can't be the literal \"random\"."); return Task.CompletedTask; } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs index 9da7cc99..27d6e844 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs @@ -30,7 +30,8 @@ namespace Kyoo.Core.Controllers; /// /// A local repository to handle library items. /// -public class LibraryItemRepository : DapperRepository +public class LibraryItemRepository(DbConnection database, SqlVariableContext context) + : DapperRepository(database, context) { // language=PostgreSQL protected override FormattableString Sql => @@ -78,9 +79,6 @@ public class LibraryItemRepository : DapperRepository throw new InvalidDataException(); } - public LibraryItemRepository(DbConnection database, SqlVariableContext context) - : base(database, context) { } - public async Task> GetAllOfCollection( Guid collectionId, Filter? filter = default, diff --git a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs index 4ab8b73a..c04a72cb 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs @@ -71,8 +71,8 @@ public class MovieRepository( await base.Validate(resource); if (resource.Studio != null) { - resource.Studio = await studios.CreateIfNotExists(resource.Studio); - resource.StudioId = resource.Studio.Id; + resource.StudioId = (await studios.CreateIfNotExists(resource.Studio)).Id; + resource.Studio = null; } await thumbnails.DownloadImages(resource); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs index 9d7b46bb..590d0b10 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs @@ -80,17 +80,9 @@ public class SeasonRepository(DatabaseContext database, IThumbnailsManager thumb protected override async Task Validate(Season resource) { await base.Validate(resource); + resource.Show = null; if (resource.ShowId == Guid.Empty) - { - if (resource.Show == null) - { - throw new ValidationException( - $"Can't store a season not related to any show " - + $"(showID: {resource.ShowId})." - ); - } - resource.ShowId = resource.Show.Id; - } + throw new ValidationException("Missing show id"); await thumbnails.DownloadImages(resource); } } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs index ed67e2df..17ee8251 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs @@ -60,7 +60,7 @@ public class ShowRepository( && existing.StartAir?.Year != obj.StartAir?.Year ) { - obj.Slug = $"{obj.Slug}-{obj.StartAir!.Value.Year}"; + obj.Slug = $"{obj.Slug}-{obj.AirDate!.Value.Year}"; return base.Create(obj); } } @@ -71,8 +71,8 @@ public class ShowRepository( await base.Validate(resource); if (resource.Studio != null) { - resource.Studio = await studios.CreateIfNotExists(resource.Studio); - resource.StudioId = resource.Studio.Id; + resource.StudioId = (await studios.CreateIfNotExists(resource.Studio)).Id; + resource.Studio = null; } await thumbnails.DownloadImages(resource); } From c4b42c996116f5995fdacfdfc99c364e3b6bafcd Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 19:41:36 +0200 Subject: [PATCH 21/26] Auto download missing images on startup --- back/Dockerfile | 2 +- back/Dockerfile.dev | 2 +- .../Controllers/IThumbnailsManager.cs | 2 + .../Kyoo.Core/Controllers/MiscRepository.cs | 77 +++++++++++++++++++ .../Controllers/ThumbnailsManager.cs | 8 +- back/src/Kyoo.Core/CoreModule.cs | 1 + .../Kyoo.Core/Extensions/ServiceExtensions.cs | 3 + back/src/Kyoo.Core/Program.cs | 7 +- 8 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 back/src/Kyoo.Core/Controllers/MiscRepository.cs diff --git a/back/Dockerfile b/back/Dockerfile index a29161df..b2c5e2b4 100644 --- a/back/Dockerfile +++ b/back/Dockerfile @@ -22,7 +22,7 @@ FROM mcr.microsoft.com/dotnet/aspnet:8.0 RUN apt-get update && apt-get install -y curl COPY --from=builder /app /app -WORKDIR /kyoo +WORKDIR /app EXPOSE 5000 # The back can take a long time to start if meilisearch is initializing HEALTHCHECK --interval=5s --retries=15 CMD curl --fail http://localhost:5000/health || exit diff --git a/back/Dockerfile.dev b/back/Dockerfile.dev index 6f3e1a27..e33a87a5 100644 --- a/back/Dockerfile.dev +++ b/back/Dockerfile.dev @@ -14,7 +14,7 @@ COPY src/Kyoo.RabbitMq/Kyoo.RabbitMq.csproj src/Kyoo.RabbitMq/Kyoo.RabbitMq.cspr COPY src/Kyoo.Swagger/Kyoo.Swagger.csproj src/Kyoo.Swagger/Kyoo.Swagger.csproj RUN dotnet restore -WORKDIR /kyoo +WORKDIR /app EXPOSE 5000 ENV DOTNET_USE_POLLING_FILE_WATCHER 1 # HEALTHCHECK --interval=5s CMD curl --fail http://localhost:5000/health || exit diff --git a/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs b/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs index f6da3f3b..21e68f70 100644 --- a/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs +++ b/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs @@ -28,6 +28,8 @@ public interface IThumbnailsManager Task DownloadImages(T item) where T : IThumbnails; + Task DownloadImage(Image? image, string what); + string GetImagePath(Guid imageId, ImageQuality quality); Task DeleteImages(T item) diff --git a/back/src/Kyoo.Core/Controllers/MiscRepository.cs b/back/src/Kyoo.Core/Controllers/MiscRepository.cs new file mode 100644 index 00000000..820b3571 --- /dev/null +++ b/back/src/Kyoo.Core/Controllers/MiscRepository.cs @@ -0,0 +1,77 @@ +// Kyoo - A portable and vast media library solution. +// Copyright (c) Kyoo. +// +// See AUTHORS.md and LICENSE file in the project root for full license information. +// +// Kyoo is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// Kyoo is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kyoo. If not, see . + +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Dapper; +using Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models; +using Kyoo.Postgresql; +using Microsoft.EntityFrameworkCore; + +namespace Kyoo.Core.Controllers; + +public class MiscRepository( + DatabaseContext context, + DbConnection database, + IThumbnailsManager thumbnails +) +{ + private async Task> _GetAllImages() + { + string GetSql(string type) => + $""" + select poster from {type} + union all select thumbnail from {type} + union all select logo from {type} + """; + var queries = new string[] + { + "movies", + "collections", + "shows", + "seasons", + "episodes" + }.Select(x => GetSql(x)); + string sql = string.Join(" union all ", queries); + IEnumerable ret = await database.QueryAsync(sql); + return ret.Where(x => x != null).ToArray() as Image[]; + } + + public async Task DownloadMissingImages() + { + ICollection images = await _GetAllImages(); + await Task.WhenAll( + images + .Where(x => !File.Exists(thumbnails.GetImagePath(x.Id, ImageQuality.Low))) + .Select(x => thumbnails.DownloadImage(x, x.Id.ToString())) + ); + } + + public async Task> GetRegisteredPaths() + { + return await context + .Episodes.Select(x => x.Path) + .Concat(context.Movies.Select(x => x.Path)) + .ToListAsync(); + } +} diff --git a/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs b/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs index 6a8d28a3..f60dee73 100644 --- a/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs +++ b/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs @@ -50,7 +50,7 @@ public class ThumbnailsManager( await reader.CopyToAsync(file); } - private async Task _DownloadImage(Image? image, string what) + public async Task DownloadImage(Image? image, string what) { if (image == null) return; @@ -108,9 +108,9 @@ public class ThumbnailsManager( { string name = item is IResource res ? res.Slug : "???"; - await _DownloadImage(item.Poster, $"The poster of {name}"); - await _DownloadImage(item.Thumbnail, $"The thumbnail of {name}"); - await _DownloadImage(item.Logo, $"The logo of {name}"); + await DownloadImage(item.Poster, $"The poster of {name}"); + await DownloadImage(item.Thumbnail, $"The thumbnail of {name}"); + await DownloadImage(item.Logo, $"The logo of {name}"); } /// diff --git a/back/src/Kyoo.Core/CoreModule.cs b/back/src/Kyoo.Core/CoreModule.cs index 8fbfd36c..1e853065 100644 --- a/back/src/Kyoo.Core/CoreModule.cs +++ b/back/src/Kyoo.Core/CoreModule.cs @@ -63,5 +63,6 @@ public static class CoreModule ); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); } } diff --git a/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs b/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs index 9bed18bf..452aed44 100644 --- a/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs +++ b/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs @@ -21,6 +21,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using AspNetCore.Proxy; using Kyoo.Abstractions.Models.Utils; +using Kyoo.Authentication; using Kyoo.Core.Api; using Kyoo.Core.Controllers; using Kyoo.Utils; @@ -45,6 +46,8 @@ public static class ServiceExtensions options.ModelBinderProviders.Insert(0, new IncludeBinder.Provider()); options.ModelBinderProviders.Insert(0, new FilterBinder.Provider()); }) + .AddApplicationPart(typeof(CoreModule).Assembly) + .AddApplicationPart(typeof(AuthenticationModule).Assembly) .AddJsonOptions(x => { x.JsonSerializerOptions.TypeInfoResolver = new JsonKindResolver() diff --git a/back/src/Kyoo.Core/Program.cs b/back/src/Kyoo.Core/Program.cs index 83bbe127..dd4455a7 100644 --- a/back/src/Kyoo.Core/Program.cs +++ b/back/src/Kyoo.Core/Program.cs @@ -19,6 +19,7 @@ using System; using Kyoo.Authentication; using Kyoo.Core; +using Kyoo.Core.Controllers; using Kyoo.Core.Extensions; using Kyoo.Meiliseach; using Kyoo.Postgresql; @@ -69,11 +70,6 @@ AppDomain.CurrentDomain.UnhandledException += (_, ex) => Log.Fatal(ex.ExceptionObject as Exception, "Unhandled exception"); builder.Host.UseSerilog(); -builder - .Services.AddMvcCore() - .AddApplicationPart(typeof(CoreModule).Assembly) - .AddApplicationPart(typeof(AuthenticationModule).Assembly); - builder.Services.ConfigureMvc(); builder.Services.ConfigureOpenApi(); builder.ConfigureKyoo(); @@ -99,6 +95,7 @@ app.Services.GetRequiredService(); await using (AsyncServiceScope scope = app.Services.CreateAsyncScope()) { await MeilisearchModule.Initialize(scope.ServiceProvider); + await scope.ServiceProvider.GetRequiredService().DownloadMissingImages(); } app.Run(Environment.GetEnvironmentVariable("KYOO_BIND_URL") ?? "http://*:5000"); From 1e73998aa99e8093130c4ed92c9b3b1402603545 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 19:42:48 +0200 Subject: [PATCH 22/26] Make scanner use new /paths route --- .../Permission/PermissionAttribute.cs | 2 +- .../Models/Utils/Constants.cs | 1 + .../src/Kyoo.Core/Views/{ => Admin}/Health.cs | 15 +------ back/src/Kyoo.Core/Views/Admin/Misc.cs | 45 +++++++++++++++++++ scanner/providers/kyoo_client.py | 17 +------ 5 files changed, 51 insertions(+), 29 deletions(-) rename back/src/Kyoo.Core/Views/{ => Admin}/Health.cs (83%) create mode 100644 back/src/Kyoo.Core/Views/Admin/Misc.cs diff --git a/back/src/Kyoo.Abstractions/Models/Attributes/Permission/PermissionAttribute.cs b/back/src/Kyoo.Abstractions/Models/Attributes/Permission/PermissionAttribute.cs index 29aad1e4..ba1ff743 100644 --- a/back/src/Kyoo.Abstractions/Models/Attributes/Permission/PermissionAttribute.cs +++ b/back/src/Kyoo.Abstractions/Models/Attributes/Permission/PermissionAttribute.cs @@ -94,7 +94,7 @@ public class PermissionAttribute : Attribute, IFilterFactory /// /// The group of this permission. /// - public Group Group { get; } + public Group Group { get; set; } /// /// Ask a permission to run an action. diff --git a/back/src/Kyoo.Abstractions/Models/Utils/Constants.cs b/back/src/Kyoo.Abstractions/Models/Utils/Constants.cs index 6db78c51..f12c44d5 100644 --- a/back/src/Kyoo.Abstractions/Models/Utils/Constants.cs +++ b/back/src/Kyoo.Abstractions/Models/Utils/Constants.cs @@ -56,4 +56,5 @@ public static class Constants /// A group name for . It should be used for endpoints used by admins. /// public const string AdminGroup = "4:Admin"; + public const string OtherGroup = "5:Other"; } diff --git a/back/src/Kyoo.Core/Views/Health.cs b/back/src/Kyoo.Core/Views/Admin/Health.cs similarity index 83% rename from back/src/Kyoo.Core/Views/Health.cs rename to back/src/Kyoo.Core/Views/Admin/Health.cs index 7680b3b4..3fa1d7aa 100644 --- a/back/src/Kyoo.Core/Views/Health.cs +++ b/back/src/Kyoo.Core/Views/Admin/Health.cs @@ -30,19 +30,8 @@ namespace Kyoo.Core.Api; [Route("health")] [ApiController] [ApiDefinition("Health")] -public class Health : BaseApi +public class Health(HealthCheckService healthCheckService) : BaseApi { - private readonly HealthCheckService _healthCheckService; - - /// - /// Create a new . - /// - /// The service to check health. - public Health(HealthCheckService healthCheckService) - { - _healthCheckService = healthCheckService; - } - /// /// Check if the api is ready to accept requests. /// @@ -57,7 +46,7 @@ public class Health : BaseApi headers.Pragma = "no-cache"; headers.Expires = "Thu, 01 Jan 1970 00:00:00 GMT"; - HealthReport result = await _healthCheckService.CheckHealthAsync(); + HealthReport result = await healthCheckService.CheckHealthAsync(); return result.Status switch { HealthStatus.Healthy => Ok(new HealthResult("Healthy")), diff --git a/back/src/Kyoo.Core/Views/Admin/Misc.cs b/back/src/Kyoo.Core/Views/Admin/Misc.cs new file mode 100644 index 00000000..dd54fea7 --- /dev/null +++ b/back/src/Kyoo.Core/Views/Admin/Misc.cs @@ -0,0 +1,45 @@ +// Kyoo - A portable and vast media library solution. +// Copyright (c) Kyoo. +// +// See AUTHORS.md and LICENSE file in the project root for full license information. +// +// Kyoo is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// Kyoo is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kyoo. If not, see . + +using System.Collections.Generic; +using System.Threading.Tasks; +using Kyoo.Abstractions.Models.Permissions; +using Kyoo.Core.Controllers; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Kyoo.Core.Api; + +/// +/// Private APIs only used for other services. Can change at any time without notice. +/// +[ApiController] +[Permission(nameof(Misc), Kind.Read, Group = Group.Admin)] +public class Misc(MiscRepository repo) : BaseApi +{ + /// + /// List all registered paths. + /// + /// The list of paths known to Kyoo. + [HttpGet("/paths")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task> GetAllPaths() + { + return repo.GetRegisteredPaths(); + } +} diff --git a/scanner/providers/kyoo_client.py b/scanner/providers/kyoo_client.py index 47a16c93..5c305ad6 100644 --- a/scanner/providers/kyoo_client.py +++ b/scanner/providers/kyoo_client.py @@ -36,25 +36,12 @@ class KyooClient: await self.client.close() async def get_registered_paths(self) -> List[str]: - paths = None async with self.client.get( - f"{self._url}/episodes", - params={"limit": 0}, + f"{self._url}/paths", headers={"X-API-Key": self._api_key}, ) as r: r.raise_for_status() - ret = await r.json() - paths = list(x["path"] for x in ret["items"]) - - async with self.client.get( - f"{self._url}/movies", - params={"limit": 0}, - headers={"X-API-Key": self._api_key}, - ) as r: - r.raise_for_status() - ret = await r.json() - paths += list(x["path"] for x in ret["items"]) - return paths + return await r.json() async def create_issue(self, path: str, issue: str, extra: dict | None = None): async with self.client.post( From d5d0a6bda93ff67b3ab5d71547b49fc88f6825bc Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 19:42:56 +0200 Subject: [PATCH 23/26] Fix thumbnails api --- back/src/Kyoo.Core/Views/Content/ThumbnailsApi.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/back/src/Kyoo.Core/Views/Content/ThumbnailsApi.cs b/back/src/Kyoo.Core/Views/Content/ThumbnailsApi.cs index e10e4095..c56851e3 100644 --- a/back/src/Kyoo.Core/Views/Content/ThumbnailsApi.cs +++ b/back/src/Kyoo.Core/Views/Content/ThumbnailsApi.cs @@ -20,13 +20,23 @@ using System; using System.IO; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; +using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Permissions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using static Kyoo.Abstractions.Models.Utils.Constants; namespace Kyoo.Core.Api; +/// +/// Retrive images. +/// [ApiController] +[Route("thumbnails")] +[Route("images", Order = AlternativeRoute)] +[Route("image", Order = AlternativeRoute)] +[Permission(nameof(Image), Kind.Read, Group = Group.Overall)] +[ApiDefinition("Images", Group = OtherGroup)] public class ThumbnailsApi(IThumbnailsManager thumbs) : BaseApi { /// @@ -41,8 +51,7 @@ public class ThumbnailsApi(IThumbnailsManager thumbs) : BaseApi /// /// The image does not exists on kyoo. /// - [HttpGet("{identifier:id}/poster")] - [PartialPermission(Kind.Read)] + [HttpGet("{id:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public IActionResult GetPoster(Guid id, [FromQuery] ImageQuality? quality) From aca7c815a02fe52c1b459a98e3a099146a1f9e93 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 20:59:51 +0200 Subject: [PATCH 24/26] Use /api directly in the redirect response. Should use proxy rewrites --- back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs b/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs index 20201b5e..bf8aa7ca 100644 --- a/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs +++ b/back/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs @@ -55,7 +55,8 @@ public class CrudThumbsApi(IRepository repository) : CrudApi(repository if (img is null) return NotFound(); - return Redirect($"/thumbnails/{img.Id}"); + // TODO: Remove the /api and use a proxy rewrite instead. + return Redirect($"/api/thumbnails/{img.Id}"); } /// From 9e38c3f3339aa427a6a465f2d6348acac2dacffa Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 21:23:24 +0200 Subject: [PATCH 25/26] Chunk images redownload to prevent http timeouts --- .../Kyoo.Core/Controllers/MiscRepository.cs | 18 +++++++++++++----- back/src/Kyoo.Core/Program.cs | 5 +++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/back/src/Kyoo.Core/Controllers/MiscRepository.cs b/back/src/Kyoo.Core/Controllers/MiscRepository.cs index 820b3571..f3013a5e 100644 --- a/back/src/Kyoo.Core/Controllers/MiscRepository.cs +++ b/back/src/Kyoo.Core/Controllers/MiscRepository.cs @@ -27,6 +27,7 @@ using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Postgresql; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Core.Controllers; @@ -36,6 +37,12 @@ public class MiscRepository( IThumbnailsManager thumbnails ) { + public static async Task DownloadMissingImages(IServiceProvider services) + { + await using AsyncServiceScope scope = services.CreateAsyncScope(); + await scope.ServiceProvider.GetRequiredService().DownloadMissingImages(); + } + private async Task> _GetAllImages() { string GetSql(string type) => @@ -60,11 +67,12 @@ public class MiscRepository( public async Task DownloadMissingImages() { ICollection images = await _GetAllImages(); - await Task.WhenAll( - images - .Where(x => !File.Exists(thumbnails.GetImagePath(x.Id, ImageQuality.Low))) - .Select(x => thumbnails.DownloadImage(x, x.Id.ToString())) - ); + IEnumerable tasks = images + .Where(x => !File.Exists(thumbnails.GetImagePath(x.Id, ImageQuality.Low))) + .Select(x => thumbnails.DownloadImage(x, x.Id.ToString())); + // Chunk tasks to prevent http timouts + foreach (IEnumerable batch in tasks.Chunk(30)) + await Task.WhenAll(batch); } public async Task> GetRegisteredPaths() diff --git a/back/src/Kyoo.Core/Program.cs b/back/src/Kyoo.Core/Program.cs index dd4455a7..b750bb60 100644 --- a/back/src/Kyoo.Core/Program.cs +++ b/back/src/Kyoo.Core/Program.cs @@ -95,7 +95,8 @@ app.Services.GetRequiredService(); await using (AsyncServiceScope scope = app.Services.CreateAsyncScope()) { await MeilisearchModule.Initialize(scope.ServiceProvider); - await scope.ServiceProvider.GetRequiredService().DownloadMissingImages(); } +// The methods takes care of creating a scope and will download images on the background. +_ = MiscRepository.DownloadMissingImages(app.Services); -app.Run(Environment.GetEnvironmentVariable("KYOO_BIND_URL") ?? "http://*:5000"); +await app.RunAsync(Environment.GetEnvironmentVariable("KYOO_BIND_URL") ?? "http://*:5000"); From 996209d2059430debc18ee4e62a385b3cbef63f2 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 21:27:58 +0200 Subject: [PATCH 26/26] Format code --- .../Resources/Interfaces/IThumbnails.cs | 5 +--- .../Repositories/EpisodeRepository.cs | 16 ++++++---- back/src/Kyoo.Core/Program.cs | 1 + .../Kyoo.Core/Views/Content/ThumbnailsApi.cs | 1 - .../src/Kyoo.Core/Views/Resources/MovieApi.cs | 3 +- .../Migrations/20240420124608_ReworkImages.cs | 30 +++++++++++-------- scanner/matcher/matcher.py | 4 ++- 7 files changed, 34 insertions(+), 26 deletions(-) diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs index 92402690..69fbca66 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs @@ -104,10 +104,7 @@ public class Image string? src = document.RootElement.GetProperty("Source").GetString(); string? blurhash = document.RootElement.GetProperty("Blurhash").GetString(); Guid? id = document.RootElement.GetProperty("Id").GetGuid(); - return new Image(src ?? string.Empty, blurhash) - { - Id = id ?? Guid.Empty - }; + return new Image(src ?? string.Empty, blurhash) { Id = id ?? Guid.Empty }; } /// diff --git a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs index 3820102c..d3162666 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs @@ -33,8 +33,11 @@ namespace Kyoo.Core.Controllers; /// /// A local repository to handle episodes. /// -public class EpisodeRepository(DatabaseContext database, IRepository shows, IThumbnailsManager thumbnails) - : GenericRepository(database) +public class EpisodeRepository( + DatabaseContext database, + IRepository shows, + IThumbnailsManager thumbnails +) : GenericRepository(database) { static EpisodeRepository() { @@ -86,9 +89,12 @@ public class EpisodeRepository(DatabaseContext database, IRepository shows resource.Season = null; if (resource.SeasonId == null && resource.SeasonNumber != null) { - resource.SeasonId = await Database.Seasons.Where(x => - x.ShowId == resource.ShowId && x.SeasonNumber == resource.SeasonNumber - ).Select(x => x.Id).FirstOrDefaultAsync(); + resource.SeasonId = await Database + .Seasons.Where(x => + x.ShowId == resource.ShowId && x.SeasonNumber == resource.SeasonNumber + ) + .Select(x => x.Id) + .FirstOrDefaultAsync(); } await thumbnails.DownloadImages(resource); } diff --git a/back/src/Kyoo.Core/Program.cs b/back/src/Kyoo.Core/Program.cs index b750bb60..fc4660d1 100644 --- a/back/src/Kyoo.Core/Program.cs +++ b/back/src/Kyoo.Core/Program.cs @@ -96,6 +96,7 @@ await using (AsyncServiceScope scope = app.Services.CreateAsyncScope()) { await MeilisearchModule.Initialize(scope.ServiceProvider); } + // The methods takes care of creating a scope and will download images on the background. _ = MiscRepository.DownloadMissingImages(app.Services); diff --git a/back/src/Kyoo.Core/Views/Content/ThumbnailsApi.cs b/back/src/Kyoo.Core/Views/Content/ThumbnailsApi.cs index c56851e3..908a88d7 100644 --- a/back/src/Kyoo.Core/Views/Content/ThumbnailsApi.cs +++ b/back/src/Kyoo.Core/Views/Content/ThumbnailsApi.cs @@ -65,4 +65,3 @@ public class ThumbnailsApi(IThumbnailsManager thumbs) : BaseApi return PhysicalFile(Path.GetFullPath(path), "image/webp", true); } } - diff --git a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs index 03f07822..629c3d0f 100644 --- a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs @@ -40,8 +40,7 @@ namespace Kyoo.Core.Api; [ApiController] [PartialPermission(nameof(Show))] [ApiDefinition("Shows", Group = ResourcesGroup)] -public class MovieApi(ILibraryManager libraryManager) - : TranscoderApi(libraryManager.Movies) +public class MovieApi(ILibraryManager libraryManager) : TranscoderApi(libraryManager.Movies) { /// /// Get studio that made the show diff --git a/back/src/Kyoo.Postgresql/Migrations/20240420124608_ReworkImages.cs b/back/src/Kyoo.Postgresql/Migrations/20240420124608_ReworkImages.cs index 9606e2d7..699c74a9 100644 --- a/back/src/Kyoo.Postgresql/Migrations/20240420124608_ReworkImages.cs +++ b/back/src/Kyoo.Postgresql/Migrations/20240420124608_ReworkImages.cs @@ -9,23 +9,27 @@ namespace Kyoo.Postgresql.Migrations { private void MigrateImage(MigrationBuilder migrationBuilder, string table, string type) { - migrationBuilder.Sql($""" - update {table} as r set {type} = json_build_object( - 'Id', gen_random_uuid(), - 'Source', r.{type}_source, - 'Blurhash', r.{type}_blurhash - ) - where r.{type}_source is not null - """); + migrationBuilder.Sql( + $""" + update {table} as r set {type} = json_build_object( + 'Id', gen_random_uuid(), + 'Source', r.{type}_source, + 'Blurhash', r.{type}_blurhash + ) + where r.{type}_source is not null + """ + ); } private void UnMigrateImage(MigrationBuilder migrationBuilder, string table, string type) { - migrationBuilder.Sql($""" - update {table} as r - set {type}_source = r.{type}->>'Source', - {type}_blurhash = r.{type}->>'Blurhash' - """); + migrationBuilder.Sql( + $""" + update {table} as r + set {type}_source = r.{type}->>'Source', + {type}_blurhash = r.{type}->>'Blurhash' + """ + ); } /// diff --git a/scanner/matcher/matcher.py b/scanner/matcher/matcher.py index b0b76932..7b0c71e7 100644 --- a/scanner/matcher/matcher.py +++ b/scanner/matcher/matcher.py @@ -187,7 +187,9 @@ class Matcher: } current = await self._client.get(kind, kyoo_id) if self._provider.name not in current["externalId"]: - logger.error(f"Could not refresh metadata of {kind}/{kyoo_id}. Missisg provider id.") + logger.error( + f"Could not refresh metadata of {kind}/{kyoo_id}. Missisg provider id." + ) return False provider_id = current["externalId"][self._provider.name] new_value = await identify_table[kind](current, provider_id)