Skip to content

Commit 046e104

Browse files
committed
Fix STRM health checks and media source routing
1 parent ff6fb3f commit 046e104

9 files changed

Lines changed: 81 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ All notable changes to this project will be documented in this file.
1818
- Regular polling no longer recursively requeues every known child directory.
1919
- New and restored STRM files are reported separately, and partial directory failures return a non-zero exit status.
2020
- Upgrade and rebuild failures now preserve production data and produce accurate task state.
21+
- STRM health checks now read the rotated Emby key and service URLs from runtime configuration and discover a user dynamically.
22+
- NJS playback routing now normalizes non-string and repeated `MediaSourceId` query values before path lookup.
2123

2224
## [0.2.2] - 2026-06-23
2325

CHANGELOG.zh-CN.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
- 普通定时轮询不再无条件递归加入所有已知子目录。
1919
- 分开统计真正新增和缺失补回,目录部分失败时返回非零状态。
2020
- 升级与全量重建失败时保护生产数据并正确记录任务状态。
21+
- STRM 健康检查改为从运行配置读取轮换后的 Emby Key 和服务地址,并自动选择检测用户。
22+
- NJS 播放路由会先规范化非字符串或重复的 `MediaSourceId` 参数,再执行路径查询。
2123

2224
## [0.2.2] - 2026-06-23
2325

emby2alist/emby2Alist/conf.d/common/url-util.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ function getDefaultApiKey(rArgs) {
4747
// emby old TV client only use config.embyApiKey
4848
const embyApiKey = config.embyApiKey;
4949
if (!rArgs) { return embyApiKey; }
50-
return rArgs["X-Emby-Token"] ?? (rArgs.api_key ?? embyApiKey);
50+
const value = rArgs["X-Emby-Token"] ?? (rArgs.api_key ?? embyApiKey);
51+
return Array.isArray(value) ? value[0] : value;
5152
}
5253

5354
function getDeviceId(r) {
@@ -64,7 +65,8 @@ function getDeviceId(r) {
6465
}
6566

6667
function getMediaSourceId(rArgs) {
67-
return rArgs.MediaSourceId ? rArgs.MediaSourceId : rArgs.mediaSourceId;
68+
const value = rArgs.MediaSourceId ? rArgs.MediaSourceId : rArgs.mediaSourceId;
69+
return Array.isArray(value) ? value[0] : value;
6870
}
6971

7072
// r only is PlaybackInfo
@@ -164,4 +166,4 @@ export default {
164166
getFilePathPart,
165167
parseUrl,
166168
getRealIp,
167-
}
169+
}

emby2alist/emby2Alist/conf.d/common/util.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -528,11 +528,12 @@ function getItemInfo(r) {
528528
if (mediaSourceId) {
529529
// before is GUID like "3c25399d9cbb41368a5abdb71cfe3dc9", V4.9.0.25 is "mediasource_447039" fomrmat
530530
// 447039 is't main itemId, is mutiple video mediaSourceId
531-
let newMediaSourceId;
532-
if (mediaSourceId.startsWith("mediasource_")) {
533-
newMediaSourceId = mediaSourceId.replace("mediasource_", "");
531+
const normalizedMediaSourceId = String(Array.isArray(mediaSourceId) ? mediaSourceId[0] : mediaSourceId);
532+
let newMediaSourceId = normalizedMediaSourceId;
533+
if (normalizedMediaSourceId.startsWith("mediasource_")) {
534+
newMediaSourceId = normalizedMediaSourceId.replace("mediasource_", "");
534535
}
535-
itemInfoUri = `${embyHost}/Items?Ids=${newMediaSourceId ?? mediaSourceId}&Fields=Path,MediaSources&Limit=1&api_key=${api_key}`;
536+
itemInfoUri = `${embyHost}/Items?Ids=${newMediaSourceId}&Fields=Path,MediaSources&Limit=1&api_key=${api_key}`;
536537
} else {
537538
itemInfoUri = `${embyHost}/Items?Ids=${itemId}&Fields=Path,MediaSources&Limit=1&api_key=${api_key}`;
538539
}

emby2alist/emby2Alist/conf.d/emby.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,14 @@ async function fetchEmbyFilePath(itemInfoUri, itemId, Etag, mediaSourceId) {
616616
}
617617
// item.MediaSources on Emby has one, on Jellyfin has many!
618618
if (mediaSourceId) {
619-
mediaSource = item.MediaSources.find((m) => m.Id == mediaSourceId);
619+
const matchedMediaSource = item.MediaSources.find((m) => m.Id == mediaSourceId);
620+
if (matchedMediaSource) {
621+
mediaSource = matchedMediaSource;
622+
}
623+
}
624+
if (!mediaSource) {
625+
rvt.message = `error: emby_api item has no usable media source`;
626+
return rvt;
620627
}
621628
rvt.path = mediaSource.Path;
622629
rvt.itemName = item.Name;

nginx/conf.d/common/url-util.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ function getDefaultApiKey(rArgs) {
4747
// emby old TV client only use config.embyApiKey
4848
const embyApiKey = config.embyApiKey;
4949
if (!rArgs) { return embyApiKey; }
50-
return rArgs["X-Emby-Token"] ?? (rArgs.api_key ?? embyApiKey);
50+
const value = rArgs["X-Emby-Token"] ?? (rArgs.api_key ?? embyApiKey);
51+
return Array.isArray(value) ? value[0] : value;
5152
}
5253

5354
function getDeviceId(r) {
@@ -64,7 +65,8 @@ function getDeviceId(r) {
6465
}
6566

6667
function getMediaSourceId(rArgs) {
67-
return rArgs.MediaSourceId ? rArgs.MediaSourceId : rArgs.mediaSourceId;
68+
const value = rArgs.MediaSourceId ? rArgs.MediaSourceId : rArgs.mediaSourceId;
69+
return Array.isArray(value) ? value[0] : value;
6870
}
6971

7072
// r only is PlaybackInfo
@@ -164,4 +166,4 @@ export default {
164166
getFilePathPart,
165167
parseUrl,
166168
getRealIp,
167-
}
169+
}

nginx/conf.d/common/util.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -528,11 +528,12 @@ function getItemInfo(r) {
528528
if (mediaSourceId) {
529529
// before is GUID like "3c25399d9cbb41368a5abdb71cfe3dc9", V4.9.0.25 is "mediasource_447039" fomrmat
530530
// 447039 is't main itemId, is mutiple video mediaSourceId
531-
let newMediaSourceId;
532-
if (mediaSourceId.startsWith("mediasource_")) {
533-
newMediaSourceId = mediaSourceId.replace("mediasource_", "");
531+
const normalizedMediaSourceId = String(Array.isArray(mediaSourceId) ? mediaSourceId[0] : mediaSourceId);
532+
let newMediaSourceId = normalizedMediaSourceId;
533+
if (normalizedMediaSourceId.startsWith("mediasource_")) {
534+
newMediaSourceId = normalizedMediaSourceId.replace("mediasource_", "");
534535
}
535-
itemInfoUri = `${embyHost}/Items?Ids=${newMediaSourceId ?? mediaSourceId}&Fields=Path,MediaSources&Limit=1&api_key=${api_key}`;
536+
itemInfoUri = `${embyHost}/Items?Ids=${newMediaSourceId}&Fields=Path,MediaSources&Limit=1&api_key=${api_key}`;
536537
} else {
537538
itemInfoUri = `${embyHost}/Items?Ids=${itemId}&Fields=Path,MediaSources&Limit=1&api_key=${api_key}`;
538539
}

nginx/conf.d/emby.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,14 @@ async function fetchEmbyFilePath(itemInfoUri, itemId, Etag, mediaSourceId) {
616616
}
617617
// item.MediaSources on Emby has one, on Jellyfin has many!
618618
if (mediaSourceId) {
619-
mediaSource = item.MediaSources.find((m) => m.Id == mediaSourceId);
619+
const matchedMediaSource = item.MediaSources.find((m) => m.Id == mediaSourceId);
620+
if (matchedMediaSource) {
621+
mediaSource = matchedMediaSource;
622+
}
623+
}
624+
if (!mediaSource) {
625+
rvt.message = `error: emby_api item has no usable media source`;
626+
return rvt;
620627
}
621628
rvt.path = mediaSource.Path;
622629
rvt.itemName = item.Name;

scripts/strm_health_check.py

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env python3
2+
import argparse
23
import json
34
import os
45
import ssl
@@ -13,11 +14,9 @@
1314

1415
BASE_DIR = Path(__file__).resolve().parent.parent
1516
CONFIG_PATH = BASE_DIR / 'config' / 'strm-sync.yaml'
17+
RUNTIME_PATH = BASE_DIR / 'config' / 'runtime.yaml'
1618
OUT_PATH = BASE_DIR / 'data' / 'strm-health-report.json'
17-
EMBY_URL = 'http://127.0.0.1:8096/emby'
18-
TEST_URL = 'https://127.0.0.1:8095'
1919
USER_AGENT = 'VidHub/2.2.2'
20-
USER_ID = 'a5fc8f5b7cf843dc8f9c3c904f937090'
2120

2221

2322
class NoRedirect(urllib.request.HTTPRedirectHandler):
@@ -26,11 +25,26 @@ def redirect_request(self, req, fp, code, msg, headers, newurl):
2625

2726

2827
def load_cfg():
29-
return yaml.safe_load(CONFIG_PATH.read_text(encoding='utf-8')) or {}
28+
sync = yaml.safe_load(CONFIG_PATH.read_text(encoding='utf-8')) or {}
29+
runtime = yaml.safe_load(RUNTIME_PATH.read_text(encoding='utf-8')) or {} if RUNTIME_PATH.exists() else {}
30+
return sync, runtime
3031

3132

32-
def emby_api_key(cfg):
33-
return (cfg.get('emby2alist', {}) or {}).get('api_key') or 'YOUR_EMBY_API_KEY'
33+
def emby_api_key(sync_cfg, runtime_cfg):
34+
key = (sync_cfg.get('emby2alist', {}) or {}).get('api_key') or (runtime_cfg.get('emby', {}) or {}).get('api_key')
35+
if not key or key == 'YOUR_EMBY_API_KEY':
36+
raise RuntimeError('Emby API key is not configured')
37+
return key
38+
39+
40+
def service_urls(runtime_cfg):
41+
emby = (runtime_cfg.get('emby', {}) or {}).get('host', 'http://127.0.0.1:8096').rstrip('/') + '/emby'
42+
nginx = runtime_cfg.get('nginx', {}) or {}
43+
if nginx.get('https_enabled'):
44+
proxy = f"https://127.0.0.1:{int(nginx.get('https_port', 8095))}"
45+
else:
46+
proxy = f"http://127.0.0.1:{int(nginx.get('http_port', 8091))}"
47+
return emby, proxy
3448

3549

3650
def fetch_json(url: str):
@@ -39,17 +53,25 @@ def fetch_json(url: str):
3953
return json.load(r)
4054

4155

42-
def main():
43-
cfg = load_cfg()
44-
api_key = emby_api_key(cfg)
56+
def main(limit: int = 0):
57+
sync_cfg, runtime_cfg = load_cfg()
58+
api_key = emby_api_key(sync_cfg, runtime_cfg)
59+
emby_url, test_url = service_urls(runtime_cfg)
60+
users = fetch_json(emby_url + '/Users?' + urllib.parse.urlencode({'api_key': api_key}))
61+
admin = next((user for user in users if (user.get('Policy') or {}).get('IsAdministrator')), None)
62+
user = admin or (users[0] if users else None)
63+
if not user or not user.get('Id'):
64+
raise RuntimeError('No Emby user is available for playback health checks')
65+
user_id = user['Id']
4566
params = {
4667
'api_key': api_key,
4768
'Recursive': 'true',
4869
'IncludeItemTypes': 'Movie,Episode,Video',
4970
'Fields': 'Path,MediaSources',
5071
'Limit': '10000',
5172
}
52-
url = EMBY_URL + '/Items?' + urllib.parse.urlencode(params)
73+
params['UserId'] = user_id
74+
url = emby_url + '/Items?' + urllib.parse.urlencode(params)
5375
data = fetch_json(url)
5476
items = data.get('Items', [])
5577

@@ -69,6 +91,9 @@ def main():
6991
'mediaSourceId': media_source_id,
7092
'file_exists': bool(path and os.path.exists(path)),
7193
})
94+
available_strm_items = len(strm_items)
95+
if limit > 0:
96+
strm_items = strm_items[:limit]
7297

7398
opener = urllib.request.build_opener(
7499
urllib.request.HTTPSHandler(context=ssl._create_unverified_context()),
@@ -83,8 +108,8 @@ def main():
83108
continue
84109

85110
test_url = (
86-
f"{TEST_URL}/emby/videos/{item['id']}/stream.strm?AutoOpenLiveStream=false"
87-
f"&UserId={USER_ID}&MaxStreamingBitrate=500000000&reqformat=json&IsPlayback=true"
111+
f"{test_url}/emby/videos/{item['id']}/stream.strm?AutoOpenLiveStream=false"
112+
f"&UserId={user_id}&MaxStreamingBitrate=500000000&reqformat=json&IsPlayback=true"
88113
f"&api_key={api_key}&MediaSourceId={urllib.parse.quote(item['mediaSourceId'])}&Static=true"
89114
)
90115
req = urllib.request.Request(test_url, headers={'User-Agent': USER_AGENT}, method='GET')
@@ -111,6 +136,7 @@ def main():
111136

112137
report = {
113138
'generated_at': time.strftime('%Y-%m-%dT%H:%M:%S%z'),
139+
'available_strm_items': available_strm_items,
114140
'total_strm_items': len(strm_items),
115141
'healthy_redirect': len(healthy),
116142
'missing_file': len(missing),
@@ -126,7 +152,9 @@ def main():
126152

127153
if __name__ == '__main__':
128154
try:
129-
main()
155+
parser = argparse.ArgumentParser(description='Check Emby STRM files and proxy redirects.')
156+
parser.add_argument('--limit', type=int, default=0, help='Check only the first N STRM items; 0 checks all.')
157+
main(max(0, parser.parse_args().limit))
130158
except Exception as e:
131159
print(json.dumps({'ok': False, 'error': str(e)}, ensure_ascii=False), file=sys.stderr)
132160
sys.exit(1)

0 commit comments

Comments
 (0)