fix(api): file upload with library (#3213)
Some checks failed
Container / meta (analyzer) (push) Waiting to run
Container / meta (api) (push) Waiting to run
Container / meta (legacy) (push) Waiting to run
Container / meta (nginx) (push) Waiting to run
Container / meta (playout) (push) Waiting to run
Container / meta (worker) (push) Waiting to run
Container / build (push) Blocked by required conditions
Project / pre-commit (push) Waiting to run
Project / test-tools (push) Waiting to run
Release-Please / release-please (push) Waiting to run
API schema / check (push) Has been cancelled
API schema / dispatch (push) Has been cancelled
API / lint (push) Has been cancelled
API / test-with-database (bullseye) (push) Has been cancelled
API / test-with-database (focal) (push) Has been cancelled
API / test-with-database (jammy) (push) Has been cancelled
Legacy / lint (7.4) (push) Has been cancelled
Legacy / test (7.4) (push) Has been cancelled
Legacy / locale (push) Has been cancelled

### Description

Uploads with bulk_import defining --library fail with error 400 in the
REST API

The primary key of the track_type was changed from chars to a numerical
ID, and the data model expects this now in the REST endpoint. However
the bulk import still populates this field with the Char Tag.

### Testing Notes

**What I did:**

`libretime-api bulk_import --path /home/libretime/upload/ --library POD
--allowed-extensions mp3`

**How you can replicate my testing:**

see above

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Thomas Göttgens 2025-09-19 12:38:04 +02:00 committed by GitHub
parent 6ff6de7124
commit 643504edc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 31 additions and 16 deletions

View File

@ -85,7 +85,7 @@ class Importer:
return File.objects.filter(md5=file_md5).exists() return File.objects.filter(md5=file_md5).exists()
def _upload_file(self, filepath: Path, library: Optional[str]) -> None: def _upload_file(self, filepath: Path, library_id: Optional[int]) -> None:
try: try:
resp = requests.post( resp = requests.post(
f"{self.url}/rest/media", f"{self.url}/rest/media",
@ -94,7 +94,11 @@ class Importer:
("file", (filepath.name, filepath.open("rb"))), ("file", (filepath.name, filepath.open("rb"))),
], ],
timeout=30, timeout=30,
cookies={"tt_upload": library} if library is not None else {}, cookies=(
{"tt_upload": str(library_id)}
if library_id not in (None, 0)
else {}
),
) )
resp.raise_for_status() resp.raise_for_status()
@ -105,7 +109,7 @@ class Importer:
logger.info("deleting %s", filepath) logger.info("deleting %s", filepath)
filepath.unlink() filepath.unlink()
def _handle_file(self, filepath: Path, library: Optional[str]) -> None: def _handle_file(self, filepath: Path, library_id: Optional[int]) -> None:
logger.debug("handling file %s", filepath) logger.debug("handling file %s", filepath)
if not filepath.is_file(): if not filepath.is_file():
@ -117,7 +121,7 @@ class Importer:
self._delete_file(filepath) self._delete_file(filepath)
return return
self._upload_file(filepath, library) self._upload_file(filepath, library_id)
if self.delete_after_upload: if self.delete_after_upload:
self._delete_file(filepath) self._delete_file(filepath)
@ -125,7 +129,7 @@ class Importer:
def _walk_dir( def _walk_dir(
self, self,
path: Path, path: Path,
library: Optional[str], library_id: Optional[int],
allowed_extensions: List[str], allowed_extensions: List[str],
) -> None: ) -> None:
if not path.is_dir(): if not path.is_dir():
@ -133,13 +137,13 @@ class Importer:
for sub_path in path.iterdir(): for sub_path in path.iterdir():
if sub_path.is_dir(): if sub_path.is_dir():
self._walk_dir(sub_path, library, allowed_extensions) self._walk_dir(sub_path, library_id, allowed_extensions)
continue continue
if sub_path.suffix.lower() not in allowed_extensions: if sub_path.suffix.lower() not in allowed_extensions:
continue continue
self._handle_file(sub_path.resolve(), library) self._handle_file(sub_path.resolve(), library_id)
def _check_library(self, library: str) -> bool: def _check_library(self, library: str) -> bool:
return Library.objects.filter(code=library).exists() return Library.objects.filter(code=library).exists()
@ -153,8 +157,16 @@ class Importer:
if library is not None and not self._check_library(library): if library is not None and not self._check_library(library):
raise ValueError(f"provided library {library} does not exist") raise ValueError(f"provided library {library} does not exist")
if library:
try:
library_id = Library.objects.get(code=library).id
except Library.DoesNotExist as exc:
raise ValueError(f"provided library {library} does not exist") from exc
else:
library_id = 0
allowed_extensions = [ allowed_extensions = [
(x if x.startswith(".") else "." + x) for x in allowed_extensions (x if x.startswith(".") else "." + x) for x in allowed_extensions
] ]
self._walk_dir(path, library, allowed_extensions) self._walk_dir(path, library_id, allowed_extensions)

View File

@ -22,6 +22,8 @@ class Library(models.Model):
db_column="analyze_cue_points", db_column="analyze_cue_points",
) )
id = models.AutoField(primary_key=True)
class Meta: class Meta:
managed = False managed = False
db_table = "cc_track_types" db_table = "cc_track_types"

View File

@ -30,6 +30,7 @@ def _import_paths(tmp_path: Path):
def _library(): def _library():
return baker.make( return baker.make(
"storage.Library", "storage.Library",
id=1,
code="MUS", code="MUS",
name="Music", name="Music",
description="Some music", description="Some music",
@ -62,8 +63,8 @@ def test_importer(
): ):
importer.import_dir(import_paths[0], library.code, [".mp3"]) importer.import_dir(import_paths[0], library.code, [".mp3"])
importer._handle_file.assert_called_with(import_paths[1], library.code) importer._handle_file.assert_called_with(import_paths[1], library.id)
importer._upload_file.assert_called_with(import_paths[1], library.code) importer._upload_file.assert_called_with(import_paths[1], library.id)
importer._delete_file.assert_not_called() importer._delete_file.assert_not_called()
@ -76,8 +77,8 @@ def test_importer_and_delete(
importer.delete_after_upload = True importer.delete_after_upload = True
importer.import_dir(import_paths[0], library.code, [".mp3"]) importer.import_dir(import_paths[0], library.code, [".mp3"])
importer._handle_file.assert_called_with(import_paths[1], library.code) importer._handle_file.assert_called_with(import_paths[1], library.id)
importer._upload_file.assert_called_with(import_paths[1], library.code) importer._upload_file.assert_called_with(import_paths[1], library.id)
importer._delete_file.assert_called_with(import_paths[1]) importer._delete_file.assert_called_with(import_paths[1])
@ -87,11 +88,11 @@ def test_importer_existing_file(
importer: MockImporter, importer: MockImporter,
library, library,
): ):
baker.make("storage.File", md5="46305a7cf42ee53976c88d337e47e940") baker.make("storage.File", id=1, md5="46305a7cf42ee53976c88d337e47e940")
importer.import_dir(import_paths[0], library.code, [".mp3"]) importer.import_dir(import_paths[0], library.code, [".mp3"])
importer._handle_file.assert_called_with(import_paths[1], library.code) importer._handle_file.assert_called_with(import_paths[1], library.id)
importer._upload_file.assert_not_called() importer._upload_file.assert_not_called()
importer._delete_file.assert_not_called() importer._delete_file.assert_not_called()
@ -102,12 +103,12 @@ def test_importer_existing_file_and_delete(
importer: MockImporter, importer: MockImporter,
library, library,
): ):
baker.make("storage.File", md5="46305a7cf42ee53976c88d337e47e940") baker.make("storage.File", id=1, md5="46305a7cf42ee53976c88d337e47e940")
importer.delete_if_exists = True importer.delete_if_exists = True
importer.import_dir(import_paths[0], library.code, [".mp3"]) importer.import_dir(import_paths[0], library.code, [".mp3"])
importer._handle_file.assert_called_with(import_paths[1], library.code) importer._handle_file.assert_called_with(import_paths[1], library.id)
importer._upload_file.assert_not_called() importer._upload_file.assert_not_called()
importer._delete_file.assert_called_with(import_paths[1]) importer._delete_file.assert_called_with(import_paths[1])