Sync all projects
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user