240 lines
8.5 KiB
JavaScript
240 lines
8.5 KiB
JavaScript
const config = {
|
|
host: 'http://zqq.i234.me:12345',/*Emby服务器URL (例如 http://localhost:8096)*/
|
|
userId: "40a02f8503ce4de49d58331a282dcea1",/*Emby_userId (通过账号密码获取)*/
|
|
token: "664785413703444e8478400d91acdbbc",/*Emby_token (通过账号密码获取)*/
|
|
deviceId: "ea27caf7-9a51-4209-b1a5-374bf30c2ffd",
|
|
clientVersion: "4.9.0.31"
|
|
};
|
|
|
|
const COMMON_FIELDS =
|
|
'BasicSyncInfo,CanDelete,Container,PrimaryImageAspectRatio,ProductionYear,CommunityRating,Status,CriticRating,EndDate,Path,Overview,Genres,People,Taglines,Studios,IsFolder';
|
|
const IMAGE_TYPES = 'Primary,Backdrop,Thumb,Banner';
|
|
|
|
const deviceProfile = {
|
|
DeviceProfile: {
|
|
MaxStaticBitrate: 140000000,
|
|
MaxStreamingBitrate: 140000000,
|
|
DirectPlayProfiles: [
|
|
{ Container: 'mp4,mkv,webm', Type: 'Video', VideoCodec: 'h264,h265,av1,vp9', AudioCodec: 'aac,mp3,opus,flac' },
|
|
{ Container: 'mp3,aac,flac,opus', Type: 'Audio' },
|
|
],
|
|
TranscodingProfiles: [
|
|
{ Container: 'mp4', Type: 'Video', VideoCodec: 'h264', AudioCodec: 'aac', Context: 'Streaming', Protocol: 'http' },
|
|
{ Container: 'aac', Type: 'Audio', Context: 'Streaming', Protocol: 'http' },
|
|
],
|
|
SubtitleProfiles: [{ Format: 'srt,ass,vtt', Method: 'External' }],
|
|
CodecProfiles: [{ Type: 'Video', Codec: 'h264', ApplyConditions: [{ Condition: 'LessThanEqual', Property: 'VideoLevel', Value: '62' }] }],
|
|
BreakOnNonKeyFrames: true,
|
|
},
|
|
};
|
|
|
|
const getHeaders = (extra = {}) => ({
|
|
'X-Emby-Client': 'Emby Web',
|
|
'X-Emby-Device-Name': 'Android WebView Android',
|
|
'X-Emby-Device-Id': config.deviceId,
|
|
'X-Emby-Client-Version': config.clientVersion,
|
|
'X-Emby-Token': config.token,
|
|
...extra,
|
|
});
|
|
|
|
const buildUrl = (endpoint, params = {}) => {
|
|
const qs = Object.entries(params)
|
|
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
|
|
.join('&');
|
|
return `${config.host}${endpoint}${qs ? (endpoint.includes('?') ? '&' : '?') + qs : ''}`;
|
|
};
|
|
|
|
const getImageUrl = (itemId, imageTag) =>
|
|
imageTag ? `${config.host}/emby/Items/${itemId}/Images/Primary?maxWidth=400&tag=${imageTag}&quality=90` : '';
|
|
|
|
const extractVideos = (jsonData) =>
|
|
(jsonData?.Items || []).map((it) => ({
|
|
vod_id: it.Id,
|
|
vod_name: it.Name || '',
|
|
vod_pic: getImageUrl(it.Id, it.ImageTags?.Primary),
|
|
vod_remarks: it.ProductionYear?.toString() || '',
|
|
Type: it.Type || '',
|
|
vod_tag: it.Type === 'Folder' || it.Type === 'BoxSet' ? 'folder' : 'video',
|
|
}));
|
|
|
|
const fetchApi = async (url, options = {}) => {
|
|
const resp = await req(url, {
|
|
...options,
|
|
headers: getHeaders(options.headers || {}),
|
|
});
|
|
return resp?.content ? JSON.parse(resp.content) : null;
|
|
};
|
|
|
|
const home = async () => {
|
|
const json = await fetchApi(buildUrl(`/emby/Users/${config.userId}/Views`));
|
|
const classList = (json?.Items || [])
|
|
.filter((it) => !/播放列表|相机/.test(it.Name))
|
|
.map((it) => ({ type_id: it.Id, type_name: it.Name }));
|
|
return JSON.stringify({ class: classList, filters: {}, list: [] });
|
|
};
|
|
|
|
const homeVod = async () => {
|
|
const url = buildUrl(`/emby/Users/${config.userId}/Items`, {
|
|
SortBy: 'DateCreated',
|
|
SortOrder: 'Descending',
|
|
IncludeItemTypes: 'Movie,Series',
|
|
Recursive: 'true',
|
|
Limit: 40,
|
|
Fields: COMMON_FIELDS,
|
|
EnableImageTypes: IMAGE_TYPES,
|
|
ImageTypeLimit: 1,
|
|
});
|
|
const json = await fetchApi(url);
|
|
return JSON.stringify({ list: json ? extractVideos(json) : [] });
|
|
};
|
|
|
|
const category = async (tid, pg = 1) => {
|
|
const startIndex = (pg - 1) * 30;
|
|
const url = buildUrl(`/emby/Users/${config.userId}/Items`, {
|
|
SortBy: 'DateLastContentAdded,SortName',
|
|
SortOrder: 'Descending',
|
|
IncludeItemTypes: 'Movie,Series',
|
|
Recursive: 'true',
|
|
Fields: COMMON_FIELDS,
|
|
StartIndex: startIndex,
|
|
ParentId: tid,
|
|
EnableImageTypes: IMAGE_TYPES,
|
|
ImageTypeLimit: 1,
|
|
Limit: 30,
|
|
EnableUserData: 'true',
|
|
});
|
|
const json = await fetchApi(url);
|
|
if (!json) return JSON.stringify({ list: [], page: +pg, pagecount: 1, limit: 30, total: 0 });
|
|
const list = extractVideos(json);
|
|
const total = json.TotalRecordCount || 0;
|
|
const pagecount = Math.ceil(total / 30) || 1;
|
|
return JSON.stringify({ list, page: +pg, pagecount, limit: 30, total });
|
|
};
|
|
|
|
const detail = async (id) => {
|
|
const info = await fetchApi(buildUrl(`/emby/Users/${config.userId}/Items/${id}`, { Fields: COMMON_FIELDS }));
|
|
if (!info) return JSON.stringify({ list: [] });
|
|
if (info.Type === 'Folder' || info.Type === 'BoxSet') {
|
|
const folderItems = await fetchApi(
|
|
buildUrl(`/emby/Users/${config.userId}/Items`, {
|
|
ParentId: id,
|
|
SortBy: 'DateLastContentAdded,SortName',
|
|
SortOrder: 'Descending',
|
|
IncludeItemTypes: 'Movie,Series,Folder',
|
|
Recursive: 'false',
|
|
Fields: COMMON_FIELDS.replace(',Overview,Genres,People,Taglines,Studios,IsFolder', ''),
|
|
EnableImageTypes: IMAGE_TYPES,
|
|
ImageTypeLimit: 1,
|
|
Limit: 100,
|
|
EnableUserData: 'true',
|
|
})
|
|
);
|
|
const list = extractVideos(folderItems);
|
|
return JSON.stringify({ list, page: 1, pagecount: 1, limit: 100, total: list.length });
|
|
}
|
|
const rating = info.CommunityRating || info.CriticRating;
|
|
const formattedRating = rating ? rating.toFixed(1) : '';
|
|
const year = info.ProductionYear?.toString() || '';
|
|
let remarks = year;
|
|
if (formattedRating) remarks = remarks ? `${remarks} / ${formattedRating}分` : `${formattedRating}分`;
|
|
const directors = (info.People || [])
|
|
.filter(p => p.Type === 'Director' || p.Role === 'Director')
|
|
.map(p => p.Name);
|
|
const actors = (info.People || [])
|
|
.filter(p => p.Type === 'Actor' || p.Role === 'Actor')
|
|
.map(p => p.Name);
|
|
const studios = (info.Studios || []).map(s => s.Name);
|
|
|
|
const VOD = {
|
|
vod_id: id,
|
|
vod_name: info.Name || '',
|
|
vod_pic: getImageUrl(id, info.ImageTags?.Primary),
|
|
vod_content: (info.Overview || '暂无简介').replace(/\xa0/g, ' ').replace(/\n\n/g, '\n').trim(),
|
|
vod_year: year,
|
|
vod_director: directors.join(' / '),
|
|
vod_actor: actors.join(' / '),
|
|
vod_area: studios.join(' / '),
|
|
vod_remarks: remarks,
|
|
vod_score: formattedRating,
|
|
vod_type: (info.Genres || []).join(' / '),
|
|
vod_play_from: '',
|
|
vod_play_url: '',
|
|
};
|
|
|
|
if (info.Type === 'Series') {
|
|
const seasons = await fetchApi(buildUrl(`/emby/Shows/${id}/Seasons`, {
|
|
UserId: config.userId,
|
|
Fields: COMMON_FIELDS,
|
|
EnableTotalRecordCount: 'false',
|
|
}));
|
|
const from = [];
|
|
const urls = [];
|
|
for (const season of seasons?.Items || []) {
|
|
from.push(season.Name);
|
|
const episodes = await fetchApi(buildUrl(`/emby/Shows/${id}/Episodes`, {
|
|
SeasonId: season.Id,
|
|
UserId: config.userId,
|
|
Fields: 'Overview,PrimaryImageAspectRatio',
|
|
ImageTypeLimit: 1,
|
|
Limit: 1000,
|
|
}));
|
|
const playlist = (episodes?.Items || []).map(e => `${e.Name}$${e.Id}`);
|
|
urls.push(playlist.join('#'));
|
|
}
|
|
VOD.vod_play_from = from.join('$$$');
|
|
VOD.vod_play_url = urls.join('$$$');
|
|
} else {
|
|
VOD.vod_play_from = 'EMBY';
|
|
VOD.vod_play_url = `${info.Name || '播放'}$${id}`;
|
|
}
|
|
|
|
return JSON.stringify({ list: [VOD] });
|
|
};
|
|
|
|
const search = async (wd, quick, pg = 1) => {
|
|
const url = buildUrl(`/emby/Users/${config.userId}/Items`, {
|
|
SearchTerm: wd,
|
|
SortBy: 'SortName',
|
|
SortOrder: 'Ascending',
|
|
Fields: 'BasicSyncInfo,CanDelete,Container,PrimaryImageAspectRatio,ProductionYear,Status,EndDate',
|
|
StartIndex: (pg - 1) * 50,
|
|
EnableImageTypes: 'Primary,Backdrop,Thumb',
|
|
ImageTypeLimit: 1,
|
|
Recursive: 'true',
|
|
GroupProgramsBySeries: 'true',
|
|
Limit: 50,
|
|
});
|
|
const json = await fetchApi(url);
|
|
return JSON.stringify({ list: json ? extractVideos(json) : [] });
|
|
};
|
|
|
|
const play = async (_, id) => {
|
|
const url = buildUrl(`/Items/${id}/PlaybackInfo`, {
|
|
UserId: config.userId,
|
|
IsPlayback: 'true',
|
|
AutoOpenLiveStream: 'false',
|
|
StartTimeTicks: 0,
|
|
MaxStreamingBitrate: 140000000,
|
|
});
|
|
const resp = await req(url, {
|
|
method: 'POST',
|
|
headers: getHeaders({ 'Content-Type': 'application/json' }),
|
|
body: JSON.stringify(deviceProfile),
|
|
});
|
|
const json = JSON.parse(resp.content);
|
|
const mediaSource = json.MediaSources?.[0];
|
|
if (!mediaSource) return JSON.stringify({ parse: 1, msg: '无可用媒体源' });
|
|
let playUrl = mediaSource.DirectStreamUrl || mediaSource.DirectPlayUrl;
|
|
if (!playUrl) return JSON.stringify({ parse: 1, msg: '无直通播放链接' });
|
|
if (playUrl.startsWith('/')) {
|
|
playUrl = config.host + playUrl;
|
|
}
|
|
return JSON.stringify({
|
|
parse: 0,
|
|
url: playUrl,
|
|
header: getHeaders(),
|
|
});
|
|
};
|
|
|
|
export default { home, homeVod, category, detail, search, play };
|