mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 04:04:21 -04:00
Fix folder renaming (#449)
This commit is contained in:
commit
7daa10ef8d
@ -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>()
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -104,30 +104,17 @@ 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}/movies?filter=path eq "{quote(path)}"',
|
f"{self._url}/paths?recursive=true&path={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(
|
||||||
f"{self._url}/{path}",
|
f"{self._url}/{path}",
|
||||||
|
@ -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),
|
||||||
)
|
)
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
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:
|
||||||
|
if isdir(file):
|
||||||
|
await scan(file, publisher, client)
|
||||||
|
else:
|
||||||
await publisher.add(file)
|
await publisher.add(file)
|
||||||
elif event == Change.deleted:
|
elif event == Change.deleted:
|
||||||
await publisher.delete(file)
|
await publisher.delete(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 remove_deleted:
|
||||||
|
deleted = [x for x in registered if x not in videos]
|
||||||
if len(deleted) != len(registered):
|
if len(deleted) != len(registered):
|
||||||
await asyncio.gather(*map(publisher.delete, deleted))
|
await asyncio.gather(*map(publisher.delete, deleted))
|
||||||
elif len(deleted) > 0:
|
elif len(deleted) > 0:
|
||||||
logger.warning("All video files are unavailable. Check your disks.")
|
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}.")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user