mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 12:14:46 -04:00
Add seasons
This commit is contained in:
parent
408873b844
commit
75fb4b5809
@ -39,7 +39,7 @@ namespace Kyoo.Abstractions.Models
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (ShowSlug != null || Show != null)
|
if (ShowSlug != null || Show?.Slug != null)
|
||||||
return GetSlug(ShowSlug ?? Show.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber);
|
return GetSlug(ShowSlug ?? Show.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber);
|
||||||
return ShowID != 0
|
return ShowID != 0
|
||||||
? GetSlug(ShowID.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber)
|
? GetSlug(ShowID.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber)
|
||||||
|
@ -97,7 +97,7 @@ namespace Kyoo.Core.Api
|
|||||||
MemberExpression propertyExpr = Expression.Property(param, property);
|
MemberExpression propertyExpr = Expression.Property(param, property);
|
||||||
|
|
||||||
ConstantExpression valueExpr = null;
|
ConstantExpression valueExpr = null;
|
||||||
bool isList = typeof(IEnumerable).IsAssignableFrom(propertyExpr.Type);
|
bool isList = typeof(IEnumerable).IsAssignableFrom(propertyExpr.Type) && propertyExpr.Type != typeof(string);
|
||||||
if (operand != "ctn" && !typeof(IResource).IsAssignableFrom(propertyExpr.Type) && !isList)
|
if (operand != "ctn" && !typeof(IResource).IsAssignableFrom(propertyExpr.Type) && !isList)
|
||||||
{
|
{
|
||||||
Type propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
Type propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
||||||
|
@ -143,7 +143,7 @@ namespace Kyoo.Host
|
|||||||
.UseKestrel(options => { options.AddServerHeader = false; })
|
.UseKestrel(options => { options.AddServerHeader = false; })
|
||||||
.UseIIS()
|
.UseIIS()
|
||||||
.UseIISIntegration()
|
.UseIISIntegration()
|
||||||
.UseUrls("http://*:5000")
|
.UseUrls(Environment.GetEnvironmentVariable("KYOO_BIND_URL") ?? "http://*:5000")
|
||||||
.UseStartup(host => PluginsStartup.FromWebHost(host, new LoggerFactory().AddSerilog()))
|
.UseStartup(host => PluginsStartup.FromWebHost(host, new LoggerFactory().AddSerilog()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -480,7 +480,7 @@ namespace Kyoo.Postgresql
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await base.SaveChangesAsync(cancellationToken);
|
return await SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
catch (DbUpdateException ex)
|
catch (DbUpdateException ex)
|
||||||
{
|
{
|
||||||
@ -489,6 +489,10 @@ namespace Kyoo.Postgresql
|
|||||||
throw new DuplicatedItemException(await getExisting());
|
throw new DuplicatedItemException(await getExisting());
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
catch (DuplicatedItemException)
|
||||||
|
{
|
||||||
|
throw new DuplicatedItemException(await getExisting());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -58,6 +58,10 @@ services:
|
|||||||
- ./.env
|
- ./.env
|
||||||
volumes:
|
volumes:
|
||||||
- db:/var/lib/postgresql/data
|
- db:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
environment:
|
||||||
|
- POSTGRES_HOST_AUTH_METHOD=trust
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
|
@ -87,7 +87,7 @@ class TheMovieDatabase(Provider):
|
|||||||
logos=[f"https://image.tmdb.org/t/p/original{company['logo_path']}"]
|
logos=[f"https://image.tmdb.org/t/p/original{company['logo_path']}"]
|
||||||
if "logo_path" in company
|
if "logo_path" in company
|
||||||
else [],
|
else [],
|
||||||
external_id={
|
external_ids={
|
||||||
self.name: MetadataID(
|
self.name: MetadataID(
|
||||||
company["id"], f"https://www.themoviedb.org/company/{company['id']}"
|
company["id"], f"https://www.themoviedb.org/company/{company['id']}"
|
||||||
)
|
)
|
||||||
@ -130,7 +130,7 @@ class TheMovieDatabase(Provider):
|
|||||||
for x in movie["genres"]
|
for x in movie["genres"]
|
||||||
if x["id"] in self.genre_map
|
if x["id"] in self.genre_map
|
||||||
],
|
],
|
||||||
external_id={
|
external_ids={
|
||||||
self.name: MetadataID(
|
self.name: MetadataID(
|
||||||
movie["id"], f"https://www.themoviedb.org/movie/{movie['id']}"
|
movie["id"], f"https://www.themoviedb.org/movie/{movie['id']}"
|
||||||
),
|
),
|
||||||
@ -166,7 +166,7 @@ class TheMovieDatabase(Provider):
|
|||||||
*,
|
*,
|
||||||
language: list[str],
|
language: list[str],
|
||||||
) -> Show:
|
) -> Show:
|
||||||
show_id = show.external_id[self.name].id
|
show_id = show.external_ids[self.name].id
|
||||||
if show.original_language not in language:
|
if show.original_language not in language:
|
||||||
language.append(show.original_language)
|
language.append(show.original_language)
|
||||||
|
|
||||||
@ -197,7 +197,7 @@ class TheMovieDatabase(Provider):
|
|||||||
for x in show["genres"]
|
for x in show["genres"]
|
||||||
if x["id"] in self.genre_map
|
if x["id"] in self.genre_map
|
||||||
],
|
],
|
||||||
external_id={
|
external_ids={
|
||||||
self.name: MetadataID(
|
self.name: MetadataID(
|
||||||
show["id"], f"https://www.themoviedb.org/tv/{show['id']}"
|
show["id"], f"https://www.themoviedb.org/tv/{show['id']}"
|
||||||
),
|
),
|
||||||
@ -260,7 +260,7 @@ class TheMovieDatabase(Provider):
|
|||||||
season_number=season["season_number"],
|
season_number=season["season_number"],
|
||||||
start_air=datetime.strptime(season["air_date"], "%Y-%m-%d").date(),
|
start_air=datetime.strptime(season["air_date"], "%Y-%m-%d").date(),
|
||||||
end_air=None,
|
end_air=None,
|
||||||
external_id={
|
external_ids={
|
||||||
self.name: MetadataID(
|
self.name: MetadataID(
|
||||||
season["id"],
|
season["id"],
|
||||||
f"https://www.themoviedb.org/tv/{show_id}/season/{season['season_number']}",
|
f"https://www.themoviedb.org/tv/{show_id}/season/{season['season_number']}",
|
||||||
@ -313,7 +313,7 @@ class TheMovieDatabase(Provider):
|
|||||||
show=PartialShow(
|
show=PartialShow(
|
||||||
name=search["name"],
|
name=search["name"],
|
||||||
original_language=search["original_language"],
|
original_language=search["original_language"],
|
||||||
external_id={
|
external_ids={
|
||||||
self.name: MetadataID(
|
self.name: MetadataID(
|
||||||
show_id, f"https://www.themoviedb.org/tv/{show_id}"
|
show_id, f"https://www.themoviedb.org/tv/{show_id}"
|
||||||
)
|
)
|
||||||
@ -327,7 +327,7 @@ class TheMovieDatabase(Provider):
|
|||||||
thumbnail=f"https://image.tmdb.org/t/p/original{episode['poster_path']}"
|
thumbnail=f"https://image.tmdb.org/t/p/original{episode['poster_path']}"
|
||||||
if "poster_path" in episode
|
if "poster_path" in episode
|
||||||
else None,
|
else None,
|
||||||
external_id={
|
external_ids={
|
||||||
self.name: MetadataID(
|
self.name: MetadataID(
|
||||||
episode["id"],
|
episode["id"],
|
||||||
f"https://www.themoviedb.org/tv/{show_id}/season/{episode['season_number']}/episode/{episode['episode_number']}",
|
f"https://www.themoviedb.org/tv/{show_id}/season/{episode['season_number']}/episode/{episode['episode_number']}",
|
||||||
|
@ -13,7 +13,7 @@ from .metadataid import MetadataID
|
|||||||
class PartialShow:
|
class PartialShow:
|
||||||
name: str
|
name: str
|
||||||
original_language: str
|
original_language: str
|
||||||
external_id: dict[str, MetadataID]
|
external_ids: dict[str, MetadataID]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -30,7 +30,7 @@ class Episode:
|
|||||||
absolute_number: Optional[int]
|
absolute_number: Optional[int]
|
||||||
release_date: Optional[date | int]
|
release_date: Optional[date | int]
|
||||||
thumbnail: Optional[str]
|
thumbnail: Optional[str]
|
||||||
external_id: dict[str, MetadataID]
|
external_ids: dict[str, MetadataID]
|
||||||
|
|
||||||
path: Optional[str] = None
|
path: Optional[str] = None
|
||||||
show_id: Optional[str] = None
|
show_id: Optional[str] = None
|
||||||
@ -43,4 +43,8 @@ class Episode:
|
|||||||
return {
|
return {
|
||||||
**asdict(self),
|
**asdict(self),
|
||||||
**asdict(self.translations[default_language]),
|
**asdict(self.translations[default_language]),
|
||||||
|
"title": self.translations[default_language].name,
|
||||||
|
# TODO: The back has bad external id support, we disable it for now
|
||||||
|
"external_ids": None,
|
||||||
|
"show": None,
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ class Movie:
|
|||||||
genres: list[Genre] = field(default_factory=list)
|
genres: list[Genre] = field(default_factory=list)
|
||||||
# TODO: handle staff
|
# TODO: handle staff
|
||||||
# staff: list[Staff]
|
# staff: list[Staff]
|
||||||
external_id: dict[str, MetadataID] = field(default_factory=dict)
|
external_ids: dict[str, MetadataID] = field(default_factory=dict)
|
||||||
|
|
||||||
translations: dict[str, MovieTranslation] = field(default_factory=dict)
|
translations: dict[str, MovieTranslation] = field(default_factory=dict)
|
||||||
|
|
||||||
@ -56,10 +56,12 @@ class Movie:
|
|||||||
),
|
),
|
||||||
"logo": next(iter(self.translations[default_language].logos), None),
|
"logo": next(iter(self.translations[default_language].logos), None),
|
||||||
"trailer": next(iter(self.translations[default_language].trailers), None),
|
"trailer": next(iter(self.translations[default_language].trailers), None),
|
||||||
"studio": next(iter(self.studios), None),
|
"studio": next((x.to_kyoo() for x in self.studios), None),
|
||||||
"release_date": None,
|
"release_date": None,
|
||||||
"startAir": format_date(self.release_date),
|
"startAir": format_date(self.release_date),
|
||||||
"title": self.translations[default_language].name,
|
"title": self.translations[default_language].name,
|
||||||
"genres": [x.to_kyoo() for x in self.genres],
|
"genres": [x.to_kyoo() for x in self.genres],
|
||||||
"isMovie": True,
|
"isMovie": True,
|
||||||
|
# TODO: The back has bad external id support, we disable it for now
|
||||||
|
"external_ids": None,
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,9 @@ class Season:
|
|||||||
season_number: int
|
season_number: int
|
||||||
start_air: Optional[date | int]
|
start_air: Optional[date | int]
|
||||||
end_air: Optional[date | int]
|
end_air: Optional[date | int]
|
||||||
external_id: dict[str, MetadataID]
|
external_ids: dict[str, MetadataID]
|
||||||
|
|
||||||
|
show_id: Optional[str] = None
|
||||||
translations: dict[str, SeasonTranslation] = field(default_factory=dict)
|
translations: dict[str, SeasonTranslation] = field(default_factory=dict)
|
||||||
|
|
||||||
def to_kyoo(self):
|
def to_kyoo(self):
|
||||||
@ -34,4 +35,7 @@ class Season:
|
|||||||
"thumbnail": next(
|
"thumbnail": next(
|
||||||
iter(self.translations[default_language].thumbnails), None
|
iter(self.translations[default_language].thumbnails), None
|
||||||
),
|
),
|
||||||
|
"title": self.translations[default_language].name,
|
||||||
|
# TODO: The back has bad external id support, we disable it for now
|
||||||
|
"external_ids": None,
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ class Show:
|
|||||||
seasons: list[Season]
|
seasons: list[Season]
|
||||||
# TODO: handle staff
|
# TODO: handle staff
|
||||||
# staff: list[Staff]
|
# staff: list[Staff]
|
||||||
external_id: dict[str, MetadataID]
|
external_ids: dict[str, MetadataID]
|
||||||
|
|
||||||
translations: dict[str, ShowTranslation] = field(default_factory=dict)
|
translations: dict[str, ShowTranslation] = field(default_factory=dict)
|
||||||
|
|
||||||
@ -59,8 +59,10 @@ class Show:
|
|||||||
),
|
),
|
||||||
"logo": next(iter(self.translations[default_language].logos), None),
|
"logo": next(iter(self.translations[default_language].logos), None),
|
||||||
"trailer": next(iter(self.translations[default_language].trailers), None),
|
"trailer": next(iter(self.translations[default_language].trailers), None),
|
||||||
"studio": next(iter(self.studios), None),
|
"studio": next((x.to_kyoo() for x in self.studios), None),
|
||||||
"title": self.translations[default_language].name,
|
"title": self.translations[default_language].name,
|
||||||
"genres": [x.to_kyoo() for x in self.genres],
|
"genres": [x.to_kyoo() for x in self.genres],
|
||||||
"seasons": [x.to_kyoo() for x in self.seasons],
|
"seasons": None,
|
||||||
|
# TODO: The back has bad external id support, we disable it for now
|
||||||
|
"external_ids": None,
|
||||||
}
|
}
|
||||||
|
@ -7,4 +7,11 @@ from .metadataid import MetadataID
|
|||||||
class Studio:
|
class Studio:
|
||||||
name: str
|
name: str
|
||||||
logos: list[str] = field(default_factory=list)
|
logos: list[str] = field(default_factory=list)
|
||||||
external_id: dict[str, MetadataID] = field(default_factory=dict)
|
external_ids: dict[str, MetadataID] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def to_kyoo(self):
|
||||||
|
return {
|
||||||
|
**asdict(self),
|
||||||
|
# TODO: The back has bad external id support, we disable it for now
|
||||||
|
"external_ids": None,
|
||||||
|
}
|
||||||
|
@ -8,7 +8,7 @@ from pathlib import Path
|
|||||||
from guessit import guessit
|
from guessit import guessit
|
||||||
from providers.provider import Provider
|
from providers.provider import Provider
|
||||||
from providers.types.episode import Episode, PartialShow
|
from providers.types.episode import Episode, PartialShow
|
||||||
from providers.types.show import Show
|
from providers.types.season import Season
|
||||||
|
|
||||||
|
|
||||||
def log_errors(f):
|
def log_errors(f):
|
||||||
@ -28,6 +28,7 @@ class Scanner:
|
|||||||
) -> None:
|
) -> None:
|
||||||
self._client = client
|
self._client = client
|
||||||
self._api_key = api_key
|
self._api_key = api_key
|
||||||
|
self._url = os.environ.get("KYOO_URL", "http://back:5000")
|
||||||
self.provider = Provider.get_all(client)[0]
|
self.provider = Provider.get_all(client)[0]
|
||||||
self.cache = {"shows": {}}
|
self.cache = {"shows": {}}
|
||||||
self.languages = languages
|
self.languages = languages
|
||||||
@ -36,15 +37,30 @@ class Scanner:
|
|||||||
videos = filter(lambda p: p.is_file(), Path(path).rglob("*"))
|
videos = filter(lambda p: p.is_file(), Path(path).rglob("*"))
|
||||||
await asyncio.gather(*map(self.identify, videos))
|
await asyncio.gather(*map(self.identify, videos))
|
||||||
|
|
||||||
|
async def is_registered(self, path: Path) -> bool:
|
||||||
|
# TODO: Once movies are separated from the api, a new endpoint should be created to check for paths.
|
||||||
|
async with self._client.get(
|
||||||
|
f"{self._url}/episodes/count",
|
||||||
|
params={"path": str(path)},
|
||||||
|
headers={"X-API-Key": self._api_key},
|
||||||
|
) as r:
|
||||||
|
r.raise_for_status()
|
||||||
|
ret = await r.text()
|
||||||
|
if ret != "0":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@log_errors
|
@log_errors
|
||||||
async def identify(self, path: Path):
|
async def identify(self, path: Path):
|
||||||
|
if await self.is_registered(path):
|
||||||
|
return
|
||||||
raw = guessit(path, "--episode-prefer-number")
|
raw = guessit(path, "--episode-prefer-number")
|
||||||
logging.info("Identied %s: %s", path, raw)
|
logging.info("Identied %s: %s", path, raw)
|
||||||
|
|
||||||
# TODO: check if episode/movie already exists in kyoo and skip if it does.
|
# TODO: check if episode/movie already exists in kyoo and skip if it does.
|
||||||
# TODO: Add collections support
|
# TODO: Add collections support
|
||||||
if raw["type"] == "movie":
|
if raw["type"] == "movie":
|
||||||
return
|
|
||||||
movie = await self.provider.identify_movie(
|
movie = await self.provider.identify_movie(
|
||||||
raw["title"], raw.get("year"), language=self.languages
|
raw["title"], raw.get("year"), language=self.languages
|
||||||
)
|
)
|
||||||
@ -62,21 +78,24 @@ class Scanner:
|
|||||||
episode.path = str(path)
|
episode.path = str(path)
|
||||||
logging.debug("Got episode: %s", episode)
|
logging.debug("Got episode: %s", episode)
|
||||||
episode.show_id = await self.create_or_get_show(episode)
|
episode.show_id = await self.create_or_get_show(episode)
|
||||||
|
# TODO: Do the same things for seasons and wait for them to be created on the api (else the episode creation will fail)
|
||||||
await self.post("episodes", data=episode.to_kyoo())
|
await self.post("episodes", data=episode.to_kyoo())
|
||||||
else:
|
else:
|
||||||
logging.warn("Unknown video file type: %s", raw["type"])
|
logging.warn("Unknown video file type: %s", raw["type"])
|
||||||
|
|
||||||
async def create_or_get_show(self, episode: Episode) -> str:
|
async def create_or_get_show(self, episode: Episode) -> str:
|
||||||
provider_id = episode.show.external_id[self.provider.name].id
|
provider_id = episode.show.external_ids[self.provider.name].id
|
||||||
if provider_id in self.cache["shows"]:
|
if provider_id in self.cache["shows"]:
|
||||||
ret = self.cache["shows"][provider_id]
|
ret = self.cache["shows"][provider_id]
|
||||||
print(f"Waiting for {provider_id}")
|
|
||||||
await ret["event"].wait()
|
await ret["event"].wait()
|
||||||
if not ret["id"]:
|
if not ret["id"]:
|
||||||
raise RuntimeError("Provider failed to create the show")
|
raise RuntimeError("Provider failed to create the show")
|
||||||
return ret["id"]
|
return ret["id"]
|
||||||
|
|
||||||
self.cache["shows"][provider_id] = {"id": None, "event": asyncio.Event()}
|
self.cache["shows"][provider_id] = {"id": None, "event": asyncio.Event()}
|
||||||
|
|
||||||
|
# TODO: Check if a show with the same metadata id exists already on kyoo.
|
||||||
|
|
||||||
show = (
|
show = (
|
||||||
await self.provider.identify_show(episode.show, language=self.languages)
|
await self.provider.identify_show(episode.show, language=self.languages)
|
||||||
if isinstance(episode.show, PartialShow)
|
if isinstance(episode.show, PartialShow)
|
||||||
@ -89,14 +108,20 @@ class Scanner:
|
|||||||
# Allow tasks waiting for this show to bail out.
|
# Allow tasks waiting for this show to bail out.
|
||||||
self.cache["shows"][provider_id]["event"].set()
|
self.cache["shows"][provider_id]["event"].set()
|
||||||
raise
|
raise
|
||||||
print(f"setting {provider_id}")
|
|
||||||
self.cache["shows"][provider_id]["id"] = ret
|
self.cache["shows"][provider_id]["id"] = ret
|
||||||
self.cache["shows"][provider_id]["event"].set()
|
self.cache["shows"][provider_id]["event"].set()
|
||||||
|
|
||||||
|
# TODO: Better handling of seasons registrations (maybe a lock also)
|
||||||
|
await self.register_seasons(ret, show.seasons)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
async def register_seasons(self, show_id: str, seasons: list[Season]):
|
||||||
|
for season in seasons:
|
||||||
|
season.show_id = show_id
|
||||||
|
await self.post("seasons", data=season.to_kyoo())
|
||||||
|
|
||||||
async def post(self, path: str, *, data: object) -> str:
|
async def post(self, path: str, *, data: object) -> str:
|
||||||
url = os.environ.get("KYOO_URL", "http://back:5000")
|
logging.debug(
|
||||||
logging.info(
|
|
||||||
"Sending %s: %s",
|
"Sending %s: %s",
|
||||||
path,
|
path,
|
||||||
jsons.dumps(
|
jsons.dumps(
|
||||||
@ -106,12 +131,13 @@ class Scanner:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
async with self._client.post(
|
async with self._client.post(
|
||||||
f"{url}/{path}",
|
f"{self._url}/{path}",
|
||||||
json=data,
|
json=data,
|
||||||
headers={"X-API-Key": self._api_key},
|
headers={"X-API-Key": self._api_key},
|
||||||
) as r:
|
) as r:
|
||||||
if not r.ok:
|
# Allow 409 and continue as if it worked.
|
||||||
print(await r.text())
|
if not r.ok and r.status != 409:
|
||||||
r.raise_for_status()
|
logging.error(f"Request error: {await r.text()}")
|
||||||
|
r.raise_for_status()
|
||||||
ret = await r.json()
|
ret = await r.json()
|
||||||
return ret["id"]
|
return ret["id"]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user