feat(library): lazy materialization foundation for remote tracks (§Phase1)
Adds nullable storage fields + availability column on tracks, remote source/source_id identity on albums/artists, TrackRepository.materialize() and get_or_create_remote() repos — groundwork for on-demand YTM library (placeholders saved without audio, materialized in-place on first play). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -79,9 +79,13 @@ class MetadataEnrichmentService:
|
||||
if track.metadata_status == "manual":
|
||||
log.info("enrich_skip_manual", track_id=str(track_id))
|
||||
return EnrichmentResult(track_id=track_id, status="skipped")
|
||||
storage_uri = track.storage_uri
|
||||
if storage_uri is None:
|
||||
log.info("enrich_skip_remote", track_id=str(track_id))
|
||||
return EnrichmentResult(track_id=track_id, status="skipped")
|
||||
|
||||
tags = await self._read_local(track.storage_uri)
|
||||
match = await self._identify(track.storage_uri)
|
||||
tags = await self._read_local(storage_uri)
|
||||
match = await self._identify(storage_uri)
|
||||
|
||||
# Merge order is tag-first by default — embedded tags fix the common
|
||||
# well-tagged offline case. But a *high-confidence* AcoustID match is the
|
||||
@@ -125,7 +129,7 @@ class MetadataEnrichmentService:
|
||||
if album is not None:
|
||||
await self._resolve_cover(
|
||||
album,
|
||||
storage_uri=track.storage_uri,
|
||||
storage_uri=storage_uri,
|
||||
release_group_mbid=match.release_group_mbid if match else None,
|
||||
)
|
||||
|
||||
@@ -175,6 +179,8 @@ class MetadataEnrichmentService:
|
||||
return []
|
||||
if not self._acoustid.is_available() or not self._fingerprinter.is_available():
|
||||
return []
|
||||
if track.storage_uri is None:
|
||||
return []
|
||||
try:
|
||||
async with self._storage.as_local_path(track.storage_uri) as path:
|
||||
fingerprint = await self._fingerprinter.calculate(path)
|
||||
|
||||
@@ -72,16 +72,19 @@ class StreamingService:
|
||||
track = await self._tracks.get_by_id(track_id)
|
||||
if track is None:
|
||||
raise NotFoundError("Track not found.")
|
||||
storage_uri = track.storage_uri
|
||||
if storage_uri is None:
|
||||
raise NotFoundError("Track is not yet downloaded.")
|
||||
|
||||
stat = await self._storage.stat(track.storage_uri)
|
||||
stat = await self._storage.stat(storage_uri)
|
||||
total_size = stat.size
|
||||
content_type = stat.content_type or _FORMAT_CONTENT_TYPE.get(
|
||||
track.file_format.lower(), "application/octet-stream"
|
||||
(track.file_format or "").lower(), "application/octet-stream"
|
||||
)
|
||||
|
||||
start, end, is_partial = _parse_range(range_header, total_size)
|
||||
|
||||
stream, _ = await self._storage.open_range(track.storage_uri, start, end)
|
||||
stream, _ = await self._storage.open_range(storage_uri, start, end)
|
||||
|
||||
actual_end = end if end is not None else total_size - 1
|
||||
content_length = actual_end - start + 1
|
||||
|
||||
Reference in New Issue
Block a user