Skip to content

Commit c36295d

Browse files
authored
Merge pull request #566 from arabcoders/dev
fix: improve exception handling in status tracker
2 parents ee0942b + 90e07ce commit c36295d

File tree

18 files changed

+1276
-488
lines changed

18 files changed

+1276
-488
lines changed

.vscode/settings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
"Microformat",
125125
"microformats",
126126
"mkvtoolsnix",
127+
"MMDD",
127128
"movflags",
128129
"mpegts",
129130
"msvideo",
@@ -264,5 +265,7 @@
264265
"url": "./app/schema/tasks.json"
265266
}
266267
],
267-
"python.testing.cwd": "app/tests"
268+
"python.testing.pytestArgs": [
269+
"app"
270+
]
268271
}

API.md

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ This document describes the available endpoints and their usage. All endpoints r
3131
- [POST /api/history/cancel](#post-apihistorycancel)
3232
- [DELETE /api/history/{id}/archive](#delete-apihistoryidarchive)
3333
- [POST /api/history/{id}/archive](#post-apihistoryidarchive)
34+
- [POST /api/history/{id}/nfo](#post-apihistoryidnfo)
3435
- [GET /api/archiver](#get-apiarchiver)
3536
- [POST /api/archiver](#post-apiarchiver)
3637
- [DELETE /api/archiver](#delete-apiarchiver)
@@ -770,6 +771,58 @@ or an error:
770771

771772
---
772773

774+
### POST /api/history/{id}/nfo
775+
**Purpose**: Generate an NFO metadata file for a completed download.
776+
777+
**Path Parameter**:
778+
- `id`: Item ID from the history.
779+
780+
**Body** (optional):
781+
```json
782+
{
783+
"type": "tv",
784+
"overwrite": false
785+
}
786+
```
787+
788+
**Body Parameters**:
789+
- `type` (string, optional): NFO format type. Either `"tv"` or `"movie"`. Default: `"tv"`.
790+
- `overwrite` (boolean, optional): Overwrite existing NFO file. Default: `false`.
791+
792+
**Response** (success):
793+
```json
794+
{
795+
"message": "NFO file created",
796+
"nfo_file": "/path/to/file.nfo"
797+
}
798+
```
799+
800+
**Response** (NFO already exists):
801+
```json
802+
{
803+
"error": "NFO file already exists."
804+
}
805+
```
806+
807+
**Error Responses**:
808+
- `400 Bad Request` if:
809+
- Item has no downloaded file
810+
- `type` is not `"tv"` or `"movie"`
811+
- Failed to collect NFO data (e.g., invalid/no date)
812+
- `404 Not Found` if the item or media file doesn't exist
813+
- `409 Conflict` if NFO file already exists and `overwrite` is `false`
814+
- `500 Internal Server Error` if failed to extract metadata from URL
815+
816+
**Notes**:
817+
- Fetches fresh metadata from the source URL using yt-dlp
818+
- Creates NFO file in the same directory as the media file with `.nfo` extension
819+
- Requires a valid upload/release date in the metadata to create NFO
820+
- The NFO format follows Kodi/Jellyfin/Emby compatible structure
821+
- TV mode generates `<episodedetails>` XML
822+
- Movie mode generates `<movie>` XML
823+
824+
---
825+
773826
### GET /api/archiver
774827
**Purpose**: Read entries from the download archive associated with a preset.
775828

@@ -1523,6 +1576,13 @@ Binary image data with the appropriate `Content-Type`.
15231576
**Path Parameter**:
15241577
- `path` = Relative path within the download directory (URL-encoded).
15251578

1579+
**Query Parameters**:
1580+
- `page` (optional): Page number (1-indexed). Default: `1`.
1581+
- `per_page` (optional): Items per page. Default: `config.default_pagination`, Max: `1000`.
1582+
- `sort_by` (optional): Sort field. Options: `name`, `size`, `date`, `type`. Default: `name`.
1583+
- `sort_order` (optional): Sort direction. Options: `asc`, `desc`. Default: `asc`.
1584+
- `search` (optional): Filter by filename (case-insensitive).
1585+
15261586
**Response**:
15271587
```json
15281588
{
@@ -1534,12 +1594,12 @@ Binary image data with the appropriate `Content-Type`.
15341594
"name": "filename.mp4",
15351595
"path": "/filename.mp4",
15361596
"size": 123456789,
1537-
"mimetype": "mime/type",
1597+
"mime": "mime/type",
15381598
"mtime": "2023-01-01T12:00:00Z",
15391599
"ctime": "2023-01-01T12:00:00Z",
15401600
"is_dir": true|false,
15411601
"is_file": true|false,
1542-
...
1602+
"is_symlink": true|false
15431603
},
15441604
{
15451605
"type": "dir",
@@ -1548,11 +1608,36 @@ Binary image data with the appropriate `Content-Type`.
15481608
"path": "/Season 2025",
15491609
...
15501610
}
1551-
]
1611+
],
1612+
"pagination": {
1613+
"page": 1,
1614+
"per_page": 50,
1615+
"total": 123,
1616+
"total_pages": 3,
1617+
"has_next": true,
1618+
"has_prev": false
1619+
}
15521620
}
15531621
```
1622+
1623+
**Examples**:
1624+
```bash
1625+
# Get first page of root directory
1626+
GET /api/file/browser/
1627+
1628+
# Get second page of videos folder, sorted by size descending
1629+
GET /api/file/browser/videos?page=2&sort_by=size&sort_order=desc
1630+
1631+
# Search for mp4 files
1632+
GET /api/file/browser/videos?search=mp4
1633+
1634+
# Sort by date, newest first
1635+
GET /api/file/browser/videos?sort_by=date&sort_order=desc
1636+
```
1637+
15541638
- Returns `403 Forbidden` if file browser is disabled.
15551639
- Returns `404 Not Found` if the path doesn't exist.
1640+
- Returns `400 Bad Request` if the path is not a directory.
15561641

15571642
---
15581643

app/library/Utils.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -814,16 +814,29 @@ def get(
814814
return data
815815

816816

817-
def get_files(base_path: Path | str, dir: str | None = None):
817+
def get_files(
818+
base_path: Path | str,
819+
dir: str | None = None,
820+
page: int = 1,
821+
per_page: int = 0,
822+
sort_by: str = "name",
823+
sort_order: str = "asc",
824+
search: str | None = None,
825+
) -> tuple[list, int]:
818826
"""
819-
Get directory contents.
827+
Get directory contents with optional pagination, sorting, and search.
820828
821829
Args:
822830
base_path (Path|str): Base download path.
823831
dir (str): Directory to check.
832+
page (int): Page number (1-indexed). Ignored if per_page is 0.
833+
per_page (int): Items per page. If 0, returns all items.
834+
sort_by (str): Sort field: name, size, date, type.
835+
sort_order (str): Sort direction: asc, desc.
836+
search (str|None): Filter by filename (case-insensitive).
824837
825838
Returns:
826-
list: List of files and directories.
839+
tuple[list, int]: List of file/directory dicts and total count (before pagination).
827840
828841
Raises:
829842
OSError: If the directory is invalid or not a directory.
@@ -909,7 +922,26 @@ def get_files(base_path: Path | str, dir: str | None = None):
909922
}
910923
)
911924

912-
return contents
925+
total: int = len(contents)
926+
927+
if search:
928+
search_lower: str = search.lower()
929+
contents = [c for c in contents if search_lower in c["name"].lower()]
930+
931+
if sort_by == "name":
932+
contents.sort(key=lambda x: x["name"].lower(), reverse=(sort_order.lower() == "desc"))
933+
elif sort_by == "size":
934+
contents.sort(key=lambda x: x["size"], reverse=(sort_order.lower() == "desc"))
935+
elif sort_by == "date":
936+
contents.sort(key=lambda x: x["mtime"], reverse=(sort_order.lower() == "desc"))
937+
elif sort_by == "type":
938+
contents.sort(key=lambda x: x["content_type"], reverse=(sort_order.lower() == "desc"))
939+
940+
if per_page > 0:
941+
offset: int = (page - 1) * per_page
942+
contents = contents[offset : offset + per_page]
943+
944+
return contents, total
913945

914946

915947
def clean_item(item: dict, keys: list | tuple) -> tuple[dict, bool]:

app/library/downloads/core.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,6 @@ async def start(self) -> int | None:
301301
self.info.status = "cancelled"
302302
return ret
303303

304-
self._status_tracker.put_terminator()
305304
await self._status_tracker.drain_queue()
306305

307306
return ret

app/library/downloads/hooks.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ def progress_hook(self, data: dict[str, Any]) -> None:
4646
)
4747

4848
def postprocessor_hook(self, data: dict[str, Any]) -> None:
49+
info_dict = data.get("info_dict", {})
50+
filepath = info_dict.get("filepath")
51+
52+
status: dict[str, Any] = {
53+
"id": self.id,
54+
"action": "postprocessing",
55+
**{k: v for k, v in data.items() if k in YTDLP_PROGRESS_FIELDS},
56+
"status": "postprocessing",
57+
}
58+
if filepath:
59+
status["filepath"] = filepath
60+
4961
if self.debug:
5062
try:
5163
d_safe = create_debug_safe_dict(data)
@@ -54,14 +66,7 @@ def postprocessor_hook(self, data: dict[str, Any]) -> None:
5466
except Exception as e:
5567
self.logger.debug(f"PP Hook: Error creating debug info: {e}")
5668

57-
self.status_queue.put(
58-
{
59-
"id": self.id,
60-
"action": "postprocessing",
61-
**{k: v for k, v in data.items() if k in YTDLP_PROGRESS_FIELDS},
62-
"status": "postprocessing",
63-
}
64-
)
69+
self.status_queue.put(status)
6570

6671
def post_hook(self, filename: str) -> None:
6772
self.status_queue.put({"id": self.id, "status": "finished", "final_name": filename})

0 commit comments

Comments
 (0)