mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-22 00:32:50 +08:00
fix(music): complete subscription lifecycle
This commit is contained in:
@@ -3,6 +3,7 @@ from typing import Any, Dict, Generator, List, Optional, Tuple, Union
|
||||
from app import schemas
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.event import eventmanager
|
||||
from app.helper.mediaserver import MusicMediaServerHelper
|
||||
from app.log import logger
|
||||
from app.modules import _MediaServerBase, _ModuleBase
|
||||
from app.modules.emby.emby import Emby
|
||||
@@ -152,17 +153,14 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
|
||||
if not s:
|
||||
continue
|
||||
if mediainfo.type == MediaType.MUSIC:
|
||||
matches = s.get_music(
|
||||
title=getattr(mediainfo, "title", None),
|
||||
artist=getattr(mediainfo, "artist", None),
|
||||
album=getattr(mediainfo, "album", None),
|
||||
)
|
||||
if matches:
|
||||
matches = s.get_music(**MusicMediaServerHelper.search_params(mediainfo))
|
||||
match = MusicMediaServerHelper.find_match(mediainfo, matches)
|
||||
if match:
|
||||
return schemas.ExistMediaInfo(
|
||||
type=MediaType.MUSIC,
|
||||
server_type="emby",
|
||||
server=name,
|
||||
itemid=matches[0].item_id,
|
||||
itemid=match.item_id,
|
||||
)
|
||||
continue
|
||||
if mediainfo.type == MediaType.MOVIE:
|
||||
|
||||
@@ -9,6 +9,7 @@ from requests import Response
|
||||
|
||||
from app import schemas
|
||||
from app.core.config import settings
|
||||
from app.helper.mediaserver import MusicMediaServerHelper
|
||||
from app.log import logger
|
||||
from app.schemas import MediaServerItem
|
||||
from app.schemas.types import MediaType
|
||||
@@ -434,6 +435,8 @@ class Emby:
|
||||
url = f"{self._host}emby/Users/{self.user}/Items"
|
||||
params = {
|
||||
"IncludeItemTypes": "MusicAlbum,Audio",
|
||||
"Fields": "Album,AlbumArtist,AlbumArtists,Artists,ArtistItems,ChildCount,"
|
||||
"ProviderIds,OriginalTitle,ProductionYear,Path,ParentId",
|
||||
"SearchTerm": query,
|
||||
"Recursive": "true",
|
||||
"Limit": 20,
|
||||
@@ -747,6 +750,8 @@ class Emby:
|
||||
imdbid=item.get("ProviderIds", {}).get("Imdb"),
|
||||
tvdbid=item.get("ProviderIds", {}).get("Tvdb"),
|
||||
path=item.get("Path"),
|
||||
note=MusicMediaServerHelper.build_note(item)
|
||||
if item.get("Type") in {"MusicAlbum", "Audio"} else None,
|
||||
user_state=user_state
|
||||
|
||||
)
|
||||
@@ -819,7 +824,9 @@ class Emby:
|
||||
params = {
|
||||
"ParentId": parent,
|
||||
"api_key": self._apikey,
|
||||
"Fields": "ProviderIds,OriginalTitle,ProductionYear,Path,UserDataPlayCount,UserDataLastPlayedDate,ParentId"
|
||||
"Fields": "Album,AlbumArtist,AlbumArtists,Artists,ArtistItems,ChildCount,"
|
||||
"ProviderIds,OriginalTitle,ProductionYear,Path,UserDataPlayCount,"
|
||||
"UserDataLastPlayedDate,ParentId"
|
||||
}
|
||||
if limit is not None and limit != -1:
|
||||
params.update({
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Any, Dict, Generator, List, Optional, Tuple, Union
|
||||
from app import schemas
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.event import eventmanager
|
||||
from app.helper.mediaserver import MusicMediaServerHelper
|
||||
from app.log import logger
|
||||
from app.modules import _MediaServerBase, _ModuleBase
|
||||
from app.modules.jellyfin.jellyfin import Jellyfin
|
||||
@@ -153,17 +154,14 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
|
||||
if not s:
|
||||
continue
|
||||
if mediainfo.type == MediaType.MUSIC:
|
||||
matches = s.get_music(
|
||||
title=getattr(mediainfo, "title", None),
|
||||
artist=getattr(mediainfo, "artist", None),
|
||||
album=getattr(mediainfo, "album", None),
|
||||
)
|
||||
if matches:
|
||||
matches = s.get_music(**MusicMediaServerHelper.search_params(mediainfo))
|
||||
match = MusicMediaServerHelper.find_match(mediainfo, matches)
|
||||
if match:
|
||||
return schemas.ExistMediaInfo(
|
||||
type=MediaType.MUSIC,
|
||||
server_type="jellyfin",
|
||||
server=name,
|
||||
itemid=matches[0].item_id,
|
||||
itemid=match.item_id,
|
||||
)
|
||||
continue
|
||||
if mediainfo.type == MediaType.MOVIE:
|
||||
|
||||
@@ -7,6 +7,7 @@ from requests import Response
|
||||
|
||||
from app import schemas
|
||||
from app.core.config import settings
|
||||
from app.helper.mediaserver import MusicMediaServerHelper
|
||||
from app.log import logger
|
||||
from app.schemas import MediaType
|
||||
from app.utils.http import RequestUtils
|
||||
@@ -490,6 +491,8 @@ class Jellyfin:
|
||||
url = f"{self._host}Users/{self.user}/Items"
|
||||
params = {
|
||||
"IncludeItemTypes": "MusicAlbum,Audio",
|
||||
"Fields": "Album,AlbumArtist,AlbumArtists,Artists,ArtistItems,ChildCount,"
|
||||
"ProviderIds,OriginalTitle,ProductionYear,Path,ParentId",
|
||||
"searchTerm": query,
|
||||
"Recursive": "true",
|
||||
"Limit": 20,
|
||||
@@ -904,6 +907,8 @@ class Jellyfin:
|
||||
imdbid=item.get("ProviderIds", {}).get("Imdb"),
|
||||
tvdbid=item.get("ProviderIds", {}).get("Tvdb"),
|
||||
path=item.get("Path"),
|
||||
note=MusicMediaServerHelper.build_note(item)
|
||||
if item.get("Type") in {"MusicAlbum", "Audio"} else None,
|
||||
user_state=user_state
|
||||
|
||||
)
|
||||
@@ -977,7 +982,9 @@ class Jellyfin:
|
||||
params = {
|
||||
"ParentId": parent,
|
||||
"api_key": self._apikey,
|
||||
"Fields": "ProviderIds,OriginalTitle,ProductionYear,Path,UserDataPlayCount,UserDataLastPlayedDate,ParentId",
|
||||
"Fields": "Album,AlbumArtist,AlbumArtists,Artists,ArtistItems,ChildCount,"
|
||||
"ProviderIds,OriginalTitle,ProductionYear,Path,UserDataPlayCount,"
|
||||
"UserDataLastPlayedDate,ParentId",
|
||||
}
|
||||
if limit is not None and limit != -1:
|
||||
params.update({
|
||||
|
||||
@@ -180,7 +180,7 @@ class ListenBrainzModule(_ModuleBase):
|
||||
proxies=settings.PROXY,
|
||||
timeout=20,
|
||||
).get_res(f"{cls._base_url}{path}", params=params)
|
||||
if not response:
|
||||
if response is None:
|
||||
return None
|
||||
try:
|
||||
if response.status_code == 204:
|
||||
|
||||
@@ -775,13 +775,14 @@ class MusicBrainzModule(_ModuleBase):
|
||||
proxies=settings.PROXY,
|
||||
timeout=20,
|
||||
).get_res(f"{cls._base_url}{path}", params=params)
|
||||
if not response:
|
||||
if response is None:
|
||||
return None
|
||||
try:
|
||||
if response.status_code == 404:
|
||||
# 单曲与专辑共用同一套 ID 入口,404 属于正常的探测结果
|
||||
logger.debug(f"MusicBrainz 资源不存在:{path}")
|
||||
return None
|
||||
# 使用空对象区分稳定的不存在与瞬时请求失败,使有界缓存能够复用探测结果。
|
||||
return {}
|
||||
if response.status_code != 200:
|
||||
logger.warning(
|
||||
f"MusicBrainz 请求失败:{response.status_code} {response.text[:200]}"
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
from typing import Any, Dict, Generator, List, Optional, Tuple, Union
|
||||
|
||||
from app import schemas
|
||||
from app.core.context import MUSIC_ENTITY_ALBUM, MediaInfo
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.event import eventmanager
|
||||
from app.helper.mediaserver import MusicMediaServerHelper
|
||||
from app.log import logger
|
||||
from app.modules import _MediaServerBase, _ModuleBase
|
||||
from app.modules.navidrome.navidrome import Navidrome
|
||||
@@ -100,18 +101,6 @@ class NavidromeModule(_ModuleBase, _MediaServerBase[Navidrome]):
|
||||
return credentials
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _has_complete_album(mediainfo: MediaInfo, item: schemas.MediaServerItem) -> bool:
|
||||
"""校验 Navidrome 专辑条目的曲目数是否覆盖订阅目标。"""
|
||||
if getattr(mediainfo, "music_type", None) != MUSIC_ENTITY_ALBUM:
|
||||
return True
|
||||
try:
|
||||
expected_tracks = int(getattr(mediainfo, "total_tracks", None) or 0)
|
||||
actual_tracks = int((item.note or {}).get("song_count") or 0)
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
return False
|
||||
return expected_tracks > 0 and actual_tracks >= expected_tracks
|
||||
|
||||
def media_exists(
|
||||
self, mediainfo: MediaInfo, itemid: Optional[str] = None, server: Optional[str] = None
|
||||
) -> Optional[schemas.ExistMediaInfo]:
|
||||
@@ -127,23 +116,15 @@ class NavidromeModule(_ModuleBase, _MediaServerBase[Navidrome]):
|
||||
if not service:
|
||||
continue
|
||||
item = service.get_iteminfo(str(itemid)) if itemid else None
|
||||
if item and self._has_complete_album(mediainfo, item):
|
||||
if item and MusicMediaServerHelper.item_matches(mediainfo, item):
|
||||
return schemas.ExistMediaInfo(
|
||||
type=MediaType.MUSIC,
|
||||
server_type="navidrome",
|
||||
server=name,
|
||||
itemid=itemid,
|
||||
)
|
||||
is_album = getattr(mediainfo, "music_type", None) == MUSIC_ENTITY_ALBUM
|
||||
matches = service.search_music(
|
||||
title=None if is_album else getattr(mediainfo, "title", None),
|
||||
artist=getattr(mediainfo, "artist", None),
|
||||
album=getattr(mediainfo, "title", None) if is_album else None,
|
||||
)
|
||||
match = next(
|
||||
(candidate for candidate in matches if self._has_complete_album(mediainfo, candidate)),
|
||||
None,
|
||||
)
|
||||
matches = service.search_music(**MusicMediaServerHelper.search_params(mediainfo))
|
||||
match = MusicMediaServerHelper.find_match(mediainfo, matches)
|
||||
if match:
|
||||
return schemas.ExistMediaInfo(
|
||||
type=MediaType.MUSIC,
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Optional, Tuple, Union, Any, List, Generator, Dict
|
||||
from app import schemas
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.event import eventmanager
|
||||
from app.helper.mediaserver import MusicMediaServerHelper
|
||||
from app.log import logger
|
||||
from app.modules import _ModuleBase, _MediaServerBase
|
||||
from app.modules.plex.plex import Plex
|
||||
@@ -162,17 +163,14 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
|
||||
if not s:
|
||||
continue
|
||||
if mediainfo.type == MediaType.MUSIC:
|
||||
matches = s.get_music(
|
||||
title=getattr(mediainfo, "title", None),
|
||||
artist=getattr(mediainfo, "artist", None),
|
||||
album=getattr(mediainfo, "album", None),
|
||||
)
|
||||
if matches:
|
||||
matches = s.get_music(**MusicMediaServerHelper.search_params(mediainfo))
|
||||
match = MusicMediaServerHelper.find_match(mediainfo, matches)
|
||||
if match:
|
||||
return schemas.ExistMediaInfo(
|
||||
type=MediaType.MUSIC,
|
||||
server_type="plex",
|
||||
server=name,
|
||||
itemid=matches[0].item_id,
|
||||
itemid=match.item_id,
|
||||
)
|
||||
continue
|
||||
if mediainfo.type == MediaType.MOVIE:
|
||||
|
||||
@@ -255,7 +255,7 @@ class Plex:
|
||||
"""按歌曲、艺术家或专辑名称查询 Plex 音乐条目。"""
|
||||
if not self._plex:
|
||||
return []
|
||||
query = " ".join(filter(None, [title, artist, album])).strip()
|
||||
query = album or title or artist
|
||||
if not query:
|
||||
return []
|
||||
results: List[schemas.MediaServerItem] = []
|
||||
@@ -264,15 +264,27 @@ class Plex:
|
||||
if library.type not in ("artist", "music"):
|
||||
continue
|
||||
for item in library.search(title=query):
|
||||
item_type = getattr(item, "type", None)
|
||||
if item_type == "track":
|
||||
item_artist = getattr(item, "grandparentTitle", None)
|
||||
item_album = getattr(item, "parentTitle", None)
|
||||
else:
|
||||
item_artist = getattr(item, "parentTitle", None)
|
||||
item_album = getattr(item, "title", None) if item_type == "album" else None
|
||||
results.append(schemas.MediaServerItem(
|
||||
server="plex",
|
||||
library=library.key,
|
||||
item_id=getattr(item, "ratingKey", None) or getattr(item, "key", None),
|
||||
item_type=MediaType.MUSIC.value,
|
||||
item_type=item_type or MediaType.MUSIC.value,
|
||||
title=getattr(item, "title", None),
|
||||
original_title=getattr(item, "title", None),
|
||||
year=getattr(item, "year", None),
|
||||
path=getattr(item, "file", None),
|
||||
note={
|
||||
"artist": item_artist,
|
||||
"album": item_album,
|
||||
"song_count": getattr(item, "leafCount", None),
|
||||
},
|
||||
))
|
||||
except Exception as e:
|
||||
logger.debug(f"查询Plex音乐出错:{e}")
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Any, Generator, List, Optional, Tuple, Union
|
||||
from app import schemas
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.event import eventmanager
|
||||
from app.helper.mediaserver import MusicMediaServerHelper
|
||||
from app.log import logger
|
||||
from app.modules import _MediaServerBase, _ModuleBase
|
||||
from app.modules.trimemedia.trimemedia import TrimeMedia
|
||||
@@ -182,16 +183,15 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
|
||||
continue
|
||||
if mediainfo.type == MediaType.MUSIC:
|
||||
matches = getattr(s, "get_music", lambda **_: [])(
|
||||
title=getattr(mediainfo, "title", None),
|
||||
artist=getattr(mediainfo, "artist", None),
|
||||
album=getattr(mediainfo, "album", None),
|
||||
**MusicMediaServerHelper.search_params(mediainfo)
|
||||
)
|
||||
if matches:
|
||||
match = MusicMediaServerHelper.find_match(mediainfo, matches)
|
||||
if match:
|
||||
return schemas.ExistMediaInfo(
|
||||
type=MediaType.MUSIC,
|
||||
server_type="trimemedia",
|
||||
server=name,
|
||||
itemid=matches[0].item_id,
|
||||
itemid=match.item_id,
|
||||
)
|
||||
continue
|
||||
if mediainfo.type == MediaType.MOVIE:
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Any, Generator, List, Optional, Tuple, Union
|
||||
from app import schemas
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.event import eventmanager
|
||||
from app.helper.mediaserver import MusicMediaServerHelper
|
||||
from app.log import logger
|
||||
from app.modules import _MediaServerBase, _ModuleBase
|
||||
from app.modules.ugreen.ugreen import Ugreen
|
||||
@@ -164,16 +165,15 @@ class UgreenModule(_ModuleBase, _MediaServerBase[Ugreen]):
|
||||
continue
|
||||
if mediainfo.type == MediaType.MUSIC:
|
||||
matches = getattr(s, "get_music", lambda **_: [])(
|
||||
title=getattr(mediainfo, "title", None),
|
||||
artist=getattr(mediainfo, "artist", None),
|
||||
album=getattr(mediainfo, "album", None),
|
||||
**MusicMediaServerHelper.search_params(mediainfo)
|
||||
)
|
||||
if matches:
|
||||
match = MusicMediaServerHelper.find_match(mediainfo, matches)
|
||||
if match:
|
||||
return schemas.ExistMediaInfo(
|
||||
type=MediaType.MUSIC,
|
||||
server_type="ugreen",
|
||||
server=name,
|
||||
itemid=matches[0].item_id,
|
||||
itemid=match.item_id,
|
||||
)
|
||||
continue
|
||||
if mediainfo.type == MediaType.MOVIE:
|
||||
|
||||
@@ -7,6 +7,7 @@ from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from app import schemas
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.helper.mediaserver import MusicMediaServerHelper
|
||||
from app.log import logger
|
||||
from app.modules.ugreen.api import Api
|
||||
from app.schemas import MediaType
|
||||
@@ -667,7 +668,7 @@ class Ugreen:
|
||||
"""按歌曲、艺术家或专辑名称查询绿联影视音乐条目。"""
|
||||
if not self.is_authenticated() or not self._api:
|
||||
return []
|
||||
query = " ".join(filter(None, [album, title, artist])).strip()
|
||||
query = album or title or artist
|
||||
if not query:
|
||||
return []
|
||||
|
||||
@@ -682,6 +683,7 @@ class Ugreen:
|
||||
for info in self.__extract_video_info_list(data.get(bucket)):
|
||||
media_item = self.__build_media_server_item(info)
|
||||
if media_item:
|
||||
media_item.note = MusicMediaServerHelper.build_note(info)
|
||||
results.append(media_item)
|
||||
return results
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Any, Generator, List, Optional, Tuple, Union
|
||||
from app import schemas
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.event import eventmanager
|
||||
from app.helper.mediaserver import MusicMediaServerHelper
|
||||
from app.log import logger
|
||||
from app.modules import _MediaServerBase, _ModuleBase
|
||||
from app.modules.zspace.zspace import ZSpace
|
||||
@@ -147,16 +148,15 @@ class ZSpaceModule(_ModuleBase, _MediaServerBase[ZSpace]):
|
||||
continue
|
||||
if mediainfo.type == MediaType.MUSIC:
|
||||
matches = getattr(s, "get_music", lambda **_: [])(
|
||||
title=getattr(mediainfo, "title", None),
|
||||
artist=getattr(mediainfo, "artist", None),
|
||||
album=getattr(mediainfo, "album", None),
|
||||
**MusicMediaServerHelper.search_params(mediainfo)
|
||||
)
|
||||
if matches:
|
||||
match = MusicMediaServerHelper.find_match(mediainfo, matches)
|
||||
if match:
|
||||
return schemas.ExistMediaInfo(
|
||||
type=MediaType.MUSIC,
|
||||
server_type="zspace",
|
||||
server=name,
|
||||
itemid=matches[0].item_id,
|
||||
itemid=match.item_id,
|
||||
)
|
||||
continue
|
||||
if mediainfo.type == MediaType.MOVIE:
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import List, Optional, Union, Dict, Generator, Tuple, Any
|
||||
from requests import Response
|
||||
|
||||
from app import schemas
|
||||
from app.helper.mediaserver import MusicMediaServerHelper
|
||||
from app.log import logger
|
||||
from app.schemas import MediaServerItem
|
||||
from app.schemas.types import MediaType
|
||||
@@ -569,7 +570,8 @@ class ZSpace:
|
||||
url = f"{self._host}emby/Items"
|
||||
params = {
|
||||
"IncludeItemTypes": "MusicAlbum,Audio",
|
||||
"Fields": "ProviderIds,OriginalTitle,ProductionYear,Path,ParentId",
|
||||
"Fields": "Album,AlbumArtist,AlbumArtists,Artists,ArtistItems,ChildCount,"
|
||||
"ProviderIds,OriginalTitle,ProductionYear,Path,ParentId",
|
||||
"SearchTerm": query,
|
||||
"Recursive": "true",
|
||||
"Limit": 20,
|
||||
@@ -830,6 +832,8 @@ class ZSpace:
|
||||
imdbid=item.get("ProviderIds", {}).get("Imdb"),
|
||||
tvdbid=item.get("ProviderIds", {}).get("Tvdb"),
|
||||
path=item.get("Path"),
|
||||
note=MusicMediaServerHelper.build_note(item)
|
||||
if item.get("Type") in {"MusicAlbum", "Audio"} else None,
|
||||
user_state=user_state
|
||||
|
||||
)
|
||||
@@ -904,7 +908,8 @@ class ZSpace:
|
||||
"Recursive": "true",
|
||||
"StartIndex": current_start_index,
|
||||
"Limit": page_size,
|
||||
"Fields": "ProviderIds,OriginalTitle,ProductionYear,Path,"
|
||||
"Fields": "Album,AlbumArtist,AlbumArtists,Artists,ArtistItems,ChildCount,"
|
||||
"ProviderIds,OriginalTitle,ProductionYear,Path,"
|
||||
"UserDataPlayCount,UserDataLastPlayedDate,ParentId"
|
||||
}
|
||||
try:
|
||||
@@ -921,7 +926,10 @@ class ZSpace:
|
||||
if sub_item:
|
||||
yield sub_item
|
||||
continue
|
||||
if item.get("Type") not in ["Movie", "Series"]:
|
||||
if item.get("Type") not in ["Movie", "Series", "MusicAlbum"]:
|
||||
continue
|
||||
if item.get("Type") == "MusicAlbum":
|
||||
yield self.__format_item_info(item)
|
||||
continue
|
||||
provider_ids = item.get("ProviderIds") or {}
|
||||
needs_detail = (
|
||||
|
||||
Reference in New Issue
Block a user