Delete files via the scanner/monitor. Add an ignore folder

This commit is contained in:
Zoe Roux 2023-07-31 23:54:35 +09:00
parent f58597379b
commit 8e9cd2d2f3
5 changed files with 43 additions and 7 deletions

View File

@ -3,6 +3,9 @@ LIBRARY_ROOT=./video
CACHE_ROOT=/tmp/kyoo_cache CACHE_ROOT=/tmp/kyoo_cache
LIBRARY_LANGUAGES=en LIBRARY_LANGUAGES=en
# A pattern (regex) to ignore video files.
LIBRARY_IGNORE_PATTERN=.*/[dD]ownloads?/.*
# The following two values should be set to a random sequence of characters. # The following two values should be set to a random sequence of characters.
# You MUST change thoses when installing kyoo (for security) # You MUST change thoses when installing kyoo (for security)
AUTHENTICATION_SECRET=4c@mraGB!KRfF@kpS8739y9FcHemKxBsqqxLbdR? AUTHENTICATION_SECRET=4c@mraGB!KRfF@kpS8739y9FcHemKxBsqqxLbdR?

View File

@ -44,6 +44,8 @@ namespace Kyoo.Core.Controllers
/// </summary> /// </summary>
private readonly IProviderRepository _providers; private readonly IProviderRepository _providers;
private readonly IShowRepository _shows;
/// <inheritdoc /> /// <inheritdoc />
// Use absolute numbers by default and fallback to season/episodes if it does not exists. // Use absolute numbers by default and fallback to season/episodes if it does not exists.
protected override Sort<Episode> DefaultSort => new Sort<Episode>.Conglomerate( protected override Sort<Episode> DefaultSort => new Sort<Episode>.Conglomerate(
@ -65,6 +67,7 @@ namespace Kyoo.Core.Controllers
{ {
_database = database; _database = database;
_providers = providers; _providers = providers;
_shows = shows;
// Edit episode slugs when the show's slug changes. // Edit episode slugs when the show's slug changes.
shows.OnEdited += (show) => shows.OnEdited += (show) =>
@ -201,10 +204,13 @@ namespace Kyoo.Core.Controllers
if (obj == null) if (obj == null)
throw new ArgumentNullException(nameof(obj)); throw new ArgumentNullException(nameof(obj));
int epCount = await _database.Episodes.Where(x => x.ShowID == obj.ShowID).Take(2).CountAsync();
_database.Entry(obj).State = EntityState.Deleted; _database.Entry(obj).State = EntityState.Deleted;
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted); obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted);
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
await base.Delete(obj); await base.Delete(obj);
if (epCount == 1)
await _shows.Delete(obj.ShowID);
} }
} }
} }

View File

@ -30,8 +30,9 @@ async def main():
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
if len(sys.argv) > 1 and sys.argv[1] == "-vv": if len(sys.argv) > 1 and sys.argv[1] == "-vv":
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
logging.getLogger('watchfiles').setLevel(logging.WARNING)
jsons.set_serializer(lambda x, **_: format_date(x), Optional[date | int]) jsons.set_serializer(lambda x, **_: format_date(x), Optional[date | int]) # type: ignore
async with ClientSession( async with ClientSession(
json_serialize=lambda *args, **kwargs: jsons.dumps( json_serialize=lambda *args, **kwargs: jsons.dumps(
*args, key_transformer=jsons.KEY_TRANSFORMER_CAMELCASE, **kwargs *args, key_transformer=jsons.KEY_TRANSFORMER_CAMELCASE, **kwargs

View File

@ -10,10 +10,13 @@ async def monitor(path: str, scanner: Scanner):
try: try:
if event == Change.added: if event == Change.added:
await scanner.identify(file) await scanner.identify(file)
elif event == Change.deleted:
await scanner.delete(file);
elif event == Change.modified:
pass
else: else:
print(f"Change {event} occured for file {file}") print(f"Change {event} occured for file {file}")
except ProviderError as e: except ProviderError as e:
logging.error(str(e)) logging.error(str(e))
except Exception as e: except Exception as e:
logging.exception("Unhandled error", exc_info=e) logging.exception("Unhandled error", exc_info=e)
print("end", flush=True)

View File

@ -2,6 +2,7 @@ import os
import asyncio import asyncio
import logging import logging
import jsons import jsons
import re
from aiohttp import ClientSession from aiohttp import ClientSession
from pathlib import Path from pathlib import Path
from guessit import guessit from guessit import guessit
@ -19,6 +20,11 @@ class Scanner:
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._url = os.environ.get("KYOO_URL", "http://back:5000")
try:
self._ignore_pattern = re.compile(os.environ.get("LIBRARY_IGNORE_PATTERN", ""))
except Exception as e:
self._ignore_pattern = re.compile("")
logging.error(f"Invalid ignore pattern. Ignoring. Error: {e}")
self.provider = Provider.get_all(client)[0] self.provider = Provider.get_all(client)[0]
self.cache = {"shows": {}, "seasons": {}} self.cache = {"shows": {}, "seasons": {}}
self.languages = languages self.languages = languages
@ -26,13 +32,20 @@ class Scanner:
async def scan(self, path: str): async def scan(self, path: str):
logging.info("Starting the scan. It can take some times...") logging.info("Starting the scan. It can take some times...")
self.registered = await self.get_registered_paths() self.registered = await self.get_registered_paths()
videos = (str(p) for p in Path(path).rglob("*") if p.is_file()) videos = [str(p) for p in Path(path).rglob("*") if p.is_file()]
deleted = [x for x in self.registered if x not in videos]
if len(deleted) != len(self.registered):
for x in deleted:
await self.delete(x)
else:
logging.warning("All video files are unavailable. Check your disks.")
# We batch videos by 20 because too mutch at once kinda DDOS everything. # We batch videos by 20 because too mutch at once kinda DDOS everything.
for group in batch(videos, 20): for group in batch(iter(videos), 20):
logging.info("Batch finished. Starting a new one")
await asyncio.gather(*map(self.identify, group)) await asyncio.gather(*map(self.identify, group))
async def get_registered_paths(self) -> List[Path]: async def get_registered_paths(self) -> List[str]:
# TODO: Once movies are separated from the api, a new endpoint should be created to check for paths. # TODO: Once movies are separated from the api, a new endpoint should be created to check for paths.
async with self._client.get( async with self._client.get(
f"{self._url}/episodes", f"{self._url}/episodes",
@ -45,7 +58,7 @@ class Scanner:
@log_errors @log_errors
async def identify(self, path: str): async def identify(self, path: str):
if path in self.registered: if path in self.registered or self._ignore_pattern.match(path):
return return
raw = guessit(path, "--episode-prefer-number") raw = guessit(path, "--episode-prefer-number")
@ -139,3 +152,13 @@ class Scanner:
r.raise_for_status() r.raise_for_status()
ret = await r.json() ret = await r.json()
return ret["id"] return ret["id"]
async def delete(self, path: str):
logging.info("Deleting %s", path)
# TODO: Adapt this for movies as well when they are split
async with self._client.delete(
f"{self._url}/episodes?path={path}", headers={"X-API-Key": self._api_key}
) as r:
if not r.ok:
logging.error(f"Request error: {await r.text()}")
r.raise_for_status()