main
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3import asyncio
4
5from httpx import AsyncHTTPTransport
6from loguru import logger
7
8from emby.constant import DEVICE_ID, DEVICE_NAME, DEVICE_PROFILE, EMBY_PROXY, HEADERS, VERSION
9from networking import hx_req
10
11
12def default_headers(credentials: dict) -> dict:
13 return HEADERS | {"Authorization": f'MediaBrowser Client="Hills Windows", Device="{DEVICE_NAME}", DeviceId="{DEVICE_ID}", Version="{VERSION}", Token="{credentials["AccessToken"]}"'}
14
15
16def build_params(credentials: dict, params: dict | None = None, **kwargs) -> dict:
17 if params is None:
18 params = {}
19 params |= {
20 "X-Emby-Authorization": f'Emby Client="Hills Windows", Device="{DEVICE_NAME}", DeviceId="{DEVICE_ID}", Version="{VERSION}"',
21 "X-Emby-Client": "Hills Windows",
22 "X-Emby-Device-Name": DEVICE_NAME,
23 "X-Emby-Device-Id": DEVICE_ID,
24 "X-Emby-Client-Version": VERSION,
25 "X-Emby-Language": "zh-cn",
26 }
27 if credentials.get("AccessToken"):
28 params |= {"X-Emby-Token": credentials["AccessToken"]}
29 return params | kwargs
30
31
32async def get_user(credentials: dict) -> dict:
33 uid = credentials["User"]["Id"]
34 return await hx_req(
35 f"{credentials['Server']}/emby/Users/{uid}",
36 headers=default_headers(credentials),
37 params=build_params(credentials),
38 transport=AsyncHTTPTransport(),
39 proxy=EMBY_PROXY,
40 verify=False,
41 timeout=5,
42 check_kv={"Id": uid},
43 silent=True,
44 max_retry=0,
45 )
46
47
48async def get_resume(credentials: dict) -> dict:
49 uid = credentials["User"]["Id"]
50 params = {
51 "Fields": "ProductionYear,EndDate,Status,ProviderIds",
52 "ImageTypeLimit": 1,
53 "MediaTypes": "Video",
54 "ParentId": "",
55 "Limit": 50,
56 "StartIndex": 0,
57 "X-Emby-Token": credentials["AccessToken"],
58 }
59 return await hx_req(
60 f"{credentials['Server']}/emby/Users/{uid}/Items/Resume",
61 headers=default_headers(credentials),
62 params=build_params(credentials, params),
63 transport=AsyncHTTPTransport(),
64 proxy=EMBY_PROXY,
65 verify=False,
66 timeout=5,
67 check_keys=["Items"],
68 silent=True,
69 max_retry=1,
70 )
71
72
73async def get_item(credentials: dict, item_id: str | int) -> dict:
74 uid = credentials["User"]["Id"]
75 params = {
76 "EnableImageTypes": "Primary,Backdrop,Thumb,Logo",
77 "ImageTypeLimit": 1,
78 "Fields": "ProviderIds,ExternalUrls",
79 }
80 return await hx_req(
81 f"{credentials['Server']}/emby/Users/{uid}/Items/{item_id}",
82 headers=default_headers(credentials),
83 params=build_params(credentials, params),
84 transport=AsyncHTTPTransport(),
85 proxy=EMBY_PROXY,
86 verify=False,
87 timeout=5,
88 check_kv={"Id": item_id},
89 silent=True,
90 max_retry=1,
91 )
92
93
94async def get_items_count(credentials: dict) -> dict:
95 return await hx_req(
96 f"{credentials['Server']}/emby/Items/Counts",
97 headers=default_headers(credentials),
98 transport=AsyncHTTPTransport(),
99 proxy=EMBY_PROXY,
100 verify=False,
101 timeout=5,
102 check_keys=["MovieCount"],
103 silent=True,
104 max_retry=1,
105 )
106
107
108async def get_similar(credentials: dict, item_id: str | int) -> dict:
109 params = {
110 "UserId": credentials["User"]["Id"],
111 "Fields": "ProductionYear,EndDate,Status,CommunityRating,PrimaryImageAspectRatio,RecursiveItemCount,ProviderIds",
112 "Limit": 20,
113 "ImageTypeLimit": 1,
114 "EnableImageTypes": "Primary,Backdrop,Thumb,Logo",
115 }
116 return await hx_req(
117 f"{credentials['Server']}/emby/Items/{item_id}/Similar",
118 headers=default_headers(credentials),
119 params=build_params(credentials, params),
120 transport=AsyncHTTPTransport(),
121 proxy=EMBY_PROXY,
122 verify=False,
123 timeout=5,
124 check_keys=["Items"],
125 silent=True,
126 max_retry=1,
127 )
128
129
130async def get_playback_info(credentials: dict, item_id: str | int) -> dict:
131 return await hx_req(
132 f"{credentials['Server']}/emby/Items/{item_id}/PlaybackInfo",
133 "POST",
134 headers=default_headers(credentials) | {"content-type": "application/json"},
135 params=build_params(credentials, {"IsPlayback": "true"}),
136 transport=AsyncHTTPTransport(),
137 proxy=EMBY_PROXY,
138 verify=False,
139 timeout=5,
140 json_data=DEVICE_PROFILE,
141 check_keys=["PlaySessionId"],
142 silent=True,
143 max_retry=1,
144 )
145
146
147async def remove_from_resume(credentials: dict, item_id: str | int) -> dict:
148 server = credentials["Server"]
149 uid = credentials["User"]["Id"]
150 return await hx_req(
151 f"{server}/emby/Users/{uid}/Items/{item_id}/HideFromResume",
152 "POST",
153 headers=default_headers(credentials) | {"content-length": "0", "content-type": "application/json"},
154 params=build_params(credentials, {"Hide": "true"}),
155 transport=AsyncHTTPTransport(),
156 proxy=EMBY_PROXY,
157 verify=False,
158 timeout=5,
159 check_keys=["PlayCount"],
160 silent=True,
161 max_retry=1,
162 )
163
164
165async def get_items(credentials: dict) -> dict:
166 """Get items from emby server.
167
168 Returns:
169 dict: {item_id: item_dict}
170 """
171 server = credentials["Server"]
172 uid = credentials["User"]["Id"]
173
174 views = await hx_req(
175 f"{server}/emby/Users/{uid}/Views",
176 headers=default_headers(credentials),
177 params=build_params(credentials),
178 transport=AsyncHTTPTransport(),
179 proxy=EMBY_PROXY,
180 verify=False,
181 timeout=5,
182 check_keys=["Items"],
183 silent=True,
184 max_retry=1,
185 )
186 if views.get("hx_error"):
187 logger.error(views.get("hx_error"))
188 return {}
189 parent_ids = [x["Id"] for x in views["Items"] if x.get("CollectionType", "").lower() in ["movies", "tvshows"]]
190 items = {} # {item_id: item_dict}
191 for parent_id in parent_ids:
192 params = {
193 "ParentId": parent_id,
194 "Limit": 20,
195 "ImageTypeLimit": "1",
196 "EnableImageTypes": "Primary,Backdrop,Thumb,Logo",
197 "Fields": "ProductionYear,EndDate,Status,PrimaryImageAspectRatio,CommunityRating,RecursiveItemCount,ProviderIds",
198 }
199 resp: list[dict] = await hx_req(
200 f"{server}/emby/Users/{uid}/Items/Latest",
201 headers=default_headers(credentials),
202 params=build_params(credentials, params),
203 transport=AsyncHTTPTransport(),
204 proxy=EMBY_PROXY,
205 verify=False,
206 timeout=5,
207 check_keys=["0.Id"],
208 silent=True,
209 max_retry=1,
210 ) # type: ignore
211 if "hx_error" in resp:
212 continue
213 for item in resp:
214 if item.get("MediaType") != "Video" or not item.get("Id") or not item.get("RunTimeTicks"):
215 continue
216 items[item["Id"]] = item
217
218 if not items and parent_ids:
219 logger.warning("无法获取最新视频, 尝试从文件夹中读取.")
220 for parent_id in parent_ids:
221 await asyncio.sleep(4)
222 params = {
223 "EnableImageTypes": "Primary,Backdrop,Thumb,Logo",
224 "Fields": "BasicSyncInfo,CanDelete,PrimaryImageAspectRatio,ProductionYear",
225 "ImageTypeLimit": 1,
226 "IncludeItemTypes": "Movie",
227 "Limit": 50,
228 "ParentId": parent_id,
229 "Recursive": "true",
230 "SortBy": "SortName",
231 "SortOrder": "Ascending",
232 "StartIndex": 0,
233 }
234 resp: list[dict] = await hx_req(
235 f"{server}/emby/Users/{uid}/Items",
236 headers=default_headers(credentials),
237 params=build_params(credentials, params),
238 transport=AsyncHTTPTransport(),
239 proxy=EMBY_PROXY,
240 verify=False,
241 timeout=5,
242 silent=True,
243 max_retry=1,
244 ) # type: ignore
245 if "hx_error" in resp:
246 continue
247 for item in resp:
248 if item.get("MediaType") != "Video" or not item.get("Id") or not item.get("RunTimeTicks"):
249 continue
250 items[item["Id"]] = item
251 return items