Fix folder renaming (#449)

This commit is contained in:
Zoe Roux 2024-04-29 02:21:40 +02:00 committed by GitHub
commit 7daa10ef8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 74 additions and 35 deletions

View File

@ -84,6 +84,27 @@ public class MiscRepository(
.ToListAsync(); .ToListAsync();
} }
public async Task<int> DeletePath(string path, bool recurse)
{
// Make sure to include a path separator to prevents deletions from things like:
// DeletePath("/video/abc", true) -> /video/abdc (should not be deleted)
string dirPath = path.EndsWith("/") ? path : $"{path}/";
int count = await context
.Episodes.Where(x => x.Path == path || (recurse && x.Path.StartsWith(dirPath)))
.ExecuteDeleteAsync();
count += await context
.Movies.Where(x => x.Path == path || (recurse && x.Path.StartsWith(dirPath)))
.ExecuteDeleteAsync();
await context
.Issues.Where(x =>
x.Domain == "scanner"
&& (x.Cause == path || (recurse && x.Cause.StartsWith(dirPath)))
)
.ExecuteDeleteAsync();
return count;
}
public async Task<ICollection<RefreshableItem>> GetRefreshableItems(DateTime end) public async Task<ICollection<RefreshableItem>> GetRefreshableItems(DateTime end)
{ {
IQueryable<RefreshableItem> GetItems<T>() IQueryable<RefreshableItem> GetItems<T>()

View File

@ -30,7 +30,7 @@ namespace Kyoo.Core.Api;
/// Private APIs only used for other services. Can change at any time without notice. /// Private APIs only used for other services. Can change at any time without notice.
/// </summary> /// </summary>
[ApiController] [ApiController]
[Permission(nameof(Misc), Kind.Read, Group = Group.Admin)] [PartialPermission(nameof(Misc), Group = Group.Admin)]
public class Misc(MiscRepository repo) : BaseApi public class Misc(MiscRepository repo) : BaseApi
{ {
/// <summary> /// <summary>
@ -38,18 +38,40 @@ public class Misc(MiscRepository repo) : BaseApi
/// </summary> /// </summary>
/// <returns>The list of paths known to Kyoo.</returns> /// <returns>The list of paths known to Kyoo.</returns>
[HttpGet("/paths")] [HttpGet("/paths")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<string>> GetAllPaths() public Task<ICollection<string>> GetAllPaths()
{ {
return repo.GetRegisteredPaths(); return repo.GetRegisteredPaths();
} }
/// <summary>
/// Delete item at path.
/// </summary>
/// <param name="path">The path to delete.</param>
/// <param name="recursive">
/// If true, the path will be considered as a directory and every children will be removed.
/// </param>
/// <returns>Nothing</returns>
[HttpDelete("/paths")]
[PartialPermission(Kind.Delete)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> DeletePath(
[FromQuery] string path,
[FromQuery] bool recursive = false
)
{
await repo.DeletePath(path, recursive);
return NoContent();
}
/// <summary> /// <summary>
/// List items to refresh. /// List items to refresh.
/// </summary> /// </summary>
/// <param name="date">The upper limit for the refresh date.</param> /// <param name="date">The upper limit for the refresh date.</param>
/// <returns>The items that should be refreshed before the given date</returns> /// <returns>The items that should be refreshed before the given date</returns>
[HttpGet("/refreshables")] [HttpGet("/refreshables")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<RefreshableItem>> GetAllPaths([FromQuery] DateTime? date) public Task<ICollection<RefreshableItem>> GetAllPaths([FromQuery] DateTime? date)
{ {

View File

@ -45,10 +45,11 @@ if __name__ == "__main__":
async with ClientSession() as client: async with ClientSession() as client:
xem = TheXemClient(client) xem = TheXemClient(client)
advanced = any(x == "-a" for x in sys.argv)
ret = guessit( ret = guessit(
sys.argv[1], sys.argv[1],
xem_titles=await xem.get_expected_titles(), xem_titles=await xem.get_expected_titles(),
# extra_flags={"advanced": True}, extra_flags={"advanced": advanced},
) )
print(json.dumps(ret, cls=GuessitEncoder, indent=4)) print(json.dumps(ret, cls=GuessitEncoder, indent=4))

View File

@ -54,7 +54,7 @@ class UnlistTitles(Rule):
consequence = [RemoveMatch, AppendMatch] consequence = [RemoveMatch, AppendMatch]
def when(self, matches: Matches, context) -> Any: def when(self, matches: Matches, context) -> Any:
titles: List[Match] = matches.named("title") # type: ignore titles: List[Match] = matches.named("title", lambda x: x.tagged("title")) # type: ignore
if not titles or len(titles) <= 1: if not titles or len(titles) <= 1:
return return

View File

@ -104,29 +104,16 @@ class KyooClient:
async def delete( async def delete(
self, self,
path: str, path: str,
type: Literal["episode", "movie"] | None = None,
): ):
logger.info("Deleting %s", path) logger.info("Deleting %s", path)
if type is None or type == "movie": async with self.client.delete(
async with self.client.delete( f"{self._url}/paths?recursive=true&path={quote(path)}",
f'{self._url}/movies?filter=path eq "{quote(path)}"', headers={"X-API-Key": self._api_key},
headers={"X-API-Key": self._api_key}, ) as r:
) as r: if not r.ok:
if not r.ok: logger.error(f"Request error: {await r.text()}")
logger.error(f"Request error: {await r.text()}") r.raise_for_status()
r.raise_for_status()
if type is None or type == "episode":
async with self.client.delete(
f'{self._url}/episodes?filter=path eq "{quote(path)}"',
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()
await self.delete_issue(path)
async def get(self, path: str): async def get(self, path: str):
async with self.client.get( async with self.client.get(

View File

@ -14,7 +14,7 @@ async def main():
async with Publisher() as publisher, KyooClient() as client: async with Publisher() as publisher, KyooClient() as client:
path = os.environ.get("SCANNER_LIBRARY_ROOT", "/video") path = os.environ.get("SCANNER_LIBRARY_ROOT", "/video")
await asyncio.gather( await asyncio.gather(
monitor(path, publisher), monitor(path, publisher, client),
scan(path, publisher, client), scan(path, publisher, client, remove_deleted=True),
refresh(publisher, client), refresh(publisher, client),
) )

View File

@ -1,16 +1,21 @@
from logging import getLogger from logging import getLogger
from os.path import isdir
from watchfiles import awatch, Change from watchfiles import awatch, Change
from .publisher import Publisher from .publisher import Publisher
from .scanner import scan
from providers.kyoo_client import KyooClient
logger = getLogger(__name__) logger = getLogger(__name__)
async def monitor(path: str, publisher: Publisher): async def monitor(path: str, publisher: Publisher, client: KyooClient):
async for changes in awatch(path, ignore_permission_denied=True): async for changes in awatch(path, ignore_permission_denied=True):
for event, file in changes: for event, file in changes:
if event == Change.added: if event == Change.added:
await publisher.add(file) if isdir(file):
await scan(file, publisher, client)
else:
await publisher.add(file)
elif event == Change.deleted: elif event == Change.deleted:
await publisher.delete(file) await publisher.delete(file)
elif event == Change.modified: elif event == Change.modified:

View File

@ -9,7 +9,9 @@ from providers.kyoo_client import KyooClient
logger = getLogger(__name__) logger = getLogger(__name__)
async def scan(path: str, publisher: Publisher, client: KyooClient): async def scan(
path: str, publisher: Publisher, client: KyooClient, remove_deleted=False
):
logger.info("Starting the scan. It can take some times...") logger.info("Starting the scan. It can take some times...")
ignore_pattern = None ignore_pattern = None
try: try:
@ -25,12 +27,13 @@ async def scan(path: str, publisher: Publisher, client: KyooClient):
to_register = [ to_register = [
p for p in videos if p not in registered and not ignore_pattern.match(p) p for p in videos if p not in registered and not ignore_pattern.match(p)
] ]
deleted = [x for x in registered if x not in videos]
if len(deleted) != len(registered): if remove_deleted:
await asyncio.gather(*map(publisher.delete, deleted)) deleted = [x for x in registered if x not in videos]
elif len(deleted) > 0: if len(deleted) != len(registered):
logger.warning("All video files are unavailable. Check your disks.") await asyncio.gather(*map(publisher.delete, deleted))
elif len(deleted) > 0:
logger.warning("All video files are unavailable. Check your disks.")
await asyncio.gather(*map(publisher.add, to_register)) await asyncio.gather(*map(publisher.add, to_register))
logger.info("Scan finished.") logger.info(f"Scan finished for {path}.")