- name: Install dependencies
run: |
python -m pip install --upgrade pip build setuptools
- pip install .[test]
+ pip install . .[test]
- name: Lint/test with pre-commit
run: pre-commit run --all-files
- name: Install dependencies
run: |
python -m pip install --upgrade pip build setuptools
- pip install .[test]
+ pip install . .[test]
- name: Pytest
run: pytest --durations 10 --cov-report term-missing --cov=music_assistant --cov-report=xml tests/
from __future__ import annotations
from music_assistant.common.helpers.util import create_safe_string, create_sort_name
-from music_assistant.common.models.enums import AlbumType
from music_assistant.common.models.media_items import (
Album,
Artist,
for right_album in right_track.albums:
if compare_album(left_album, right_album):
return True
- # fallback: both albums are compilations and (near-exact) track duration match
- if (
- abs(left_track.duration - right_track.duration) <= 2
- and left_track.album.album_type in (AlbumType.UNKNOWN, AlbumType.COMPILATION)
- and right_track.album.album_type in (AlbumType.UNKNOWN, AlbumType.COMPILATION)
+ # fallback: exact album match and (near-exact) track duration match
+ if abs(left_track.duration - right_track.duration) <= 3 and compare_album(
+ left_track.album, right_track.album
):
return True
return False
return self.tags.get(key, default)
-async def parse_tags(input_file: str | AsyncGenerator[bytes, None]) -> AudioTags:
+async def parse_tags(
+ input_file: str | AsyncGenerator[bytes, None], file_size: int | None = None
+) -> AudioTags:
"""Parse tags from a media file.
input_file may be a (local) filename/url accessible by ffmpeg or
if file_path == "-":
# feed the file contents to the process
async def chunk_feeder():
- # pylint: disable=protected-access
+ bytes_written = 0
async for chunk in input_file:
try:
await proc.write(chunk)
except BrokenPipeError:
break # race-condition: read enough data for tags
+
+ # grabbing the first 5MB is enough to get the embedded tags
+ bytes_written += len(chunk)
+ if bytes_written > (5 * 1024000):
+ break
proc.write_eof()
proc.attach_task(chunk_feeder())
data = json.loads(res)
if error := data.get("error"):
raise InvalidDataError(error["string"])
- return AudioTags.parse(data)
+ tags = AudioTags.parse(data)
+ if not tags.duration and file_size and tags.bit_rate:
+ # estimate duration from filesize/bitrate
+ tags.duration = int((file_size * 8) / tags.bit_rate)
+ return tags
except (KeyError, ValueError, JSONDecodeError, InvalidDataError) as err:
raise InvalidDataError(f"Unable to retrieve info for {file_path}: {str(err)}") from err
# parse tags
input_file = file_item.local_path or self.read_file_content(file_item.absolute_path)
- tags = await parse_tags(input_file)
+ tags = await parse_tags(input_file, file_item.file_size)
name, version = parse_title_and_version(tags.title)
track = Track(
"key": "is_direct_tcp",
"type": "boolean",
"label": "Use Direct TCP",
- "default_value": false,
+ "default_value": true,
"description": "Controls whether the NetBIOS over TCP/IP (is_direct_tcp=False) or the newer Direct hosting of SMB over TCP/IP (is_direct_tcp=True) will be used for the communication. The default parameter is False which will use NetBIOS over TCP/IP for wider compatibility (TCP port: 139).",
"advanced": true,
"required": false