// @name JAVGG Mini Library const JAVGG_DEFAULT_BASE = 'https://javgg.net'; // Published in the source site's navigation; used only for trending lists. const JAVGG_TRENDING_MIRROR = 'https://javgg.co'; const JAVGG_LOGO = 'https://javgg.net/javgg.png'; const JAVGG_UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1'; const JAVGG_PAYLOAD_PREFIX = 'javgg://'; const WidgetMetadata = { id: 'javgg-mini-library', name: 'JAVGG', title: 'JAVGG', version: '1.0.10', requiredVersion: '0.0.1', author: 'Alan huang', site: JAVGG_DEFAULT_BASE, logo: JAVGG_LOGO, icon: JAVGG_LOGO, description: 'JAVGG 自定义媒体库,支持首页、分类、搜索、详情和多线路动态播放。' }; const JAVGG_SECTIONS = [ { id: 'new-post', title: '最新发布', path: '/new-post/', style: 'discover.spotlight' }, { id: 'popular-today', title: '今日热门', path: '/trending/?sort=today', style: 'discover.ranked' }, { id: 'popular-weekly', title: '本周热门', path: '/trending/?sort=weekly', style: 'discover.ranked' }, { id: 'featured', title: '精选影片', path: '/featured/', style: 'discover.posterCompact' }, { id: 'english-subtitle', title: '英文字幕', path: '/tag/english-subtitle/', style: 'discover.posterCompact' }, { id: 'chinese-subtitle', title: '中文字幕', path: '/tag/chinese-subtitle/', style: 'discover.posterCompact' }, { id: 'uncensored-leak', title: '无码流出', path: '/tag/uncensored-leak/', style: 'discover.posterCompact' }, { id: 'reduce-mosaic', title: '无码破解', path: '/tag/reduce-mosaic/', style: 'discover.posterCompact' }, { id: 'censored', title: '有码影片', path: '/tag/censored/', style: 'discover.posterCompact' }, { id: 'chinese-porn', title: '华语影片', path: '/tag/chinese-porn/', style: 'discover.posterCompact' } ]; function getManifest() { return { id: WidgetMetadata.id, name: WidgetMetadata.name, title: WidgetMetadata.title, version: WidgetMetadata.version, requiredVersion: WidgetMetadata.requiredVersion, author: WidgetMetadata.author, site: WidgetMetadata.site, logo: WidgetMetadata.logo, icon: WidgetMetadata.icon, description: WidgetMetadata.description, capabilities: { home: true, category: true, detail: true, search: true, resourceVersions: true, playback: true, aggregation: true, playbackHistory: true, resourceMatching: false }, aggregation: { search: true, playbackHistory: true, resourceMatching: false }, parameters: [{ name: 'baseUrl', title: '站点地址', type: 'input', value: JAVGG_DEFAULT_BASE, defaultValue: JAVGG_DEFAULT_BASE, required: true, description: 'JAVGG 当前可访问域名,末尾斜杠可省略。' }, { name: 'manualQuality', title: '手动画质选择', type: 'boolean', value: false, defaultValue: false, description: '默认先显示服务器,播放时选择最高可用画质。开启后需预先请求各线路清单,慢网络下可能失败。' }] }; } async function getHome(rawCtx) { const ctx = normalizeContext(rawCtx); let immediate = []; try { immediate = parseCards(ctx, await fetchText(ctx, sectionURL(ctx, JAVGG_SECTIONS[0], 1))).slice(0, 18); } catch (error) { immediate = [diagnosticItem('首页加载失败', error)]; } const browse = { id: 'javgg-browse', title: '分类浏览', style: 'discover.annualCategories', lazy: false, items: JAVGG_SECTIONS.map(categoryCard) }; const first = sectionResult(JAVGG_SECTIONS[0], immediate); return { pageType: 'home', id: 'javgg-home', title: 'JAVGG', heroAspectRatio: '2:3', hero: immediate.filter(isMediaItem).slice(0, 5), sections: [browse, first].concat(JAVGG_SECTIONS.slice(1).map(sectionShell)) }; } async function getHomeSection(rawCtx) { const ctx = normalizeContext(rawCtx); const section = findSection(firstNonEmpty(ctx.sectionId, ctx.id, ctx.pageId)) || JAVGG_SECTIONS[0]; try { const html = await fetchSectionText(ctx, section, 1); const items = parseSectionCards(ctx, html).slice(0, 18); const loadedSection = Object.assign({}, section, { title: trendingTitle(section.title, html) }); return sectionResult(loadedSection, items.length ? items : [diagnosticItem(section.title + '暂无内容')]); } catch (error) { return sectionResult(section, [diagnosticItem(section.title + '加载失败', error)]); } } async function getCategory(rawCtx) { const ctx = normalizeContext(rawCtx); const page = positiveInt(contextValue(ctx, ['page', 'pg', 'currentPage', 'pageNumber', 'pageIndex']), 1); const pageId = firstNonEmpty(ctx.pageId, ctx.id, 'new-post'); const section = findSection(pageId); const dynamicPath = decodeDynamicPageId(pageId); const title = cleanText(firstNonEmpty(ctx.title, section && section.title, dynamicPath && dynamicPath.title, 'JAVGG')); const url = section ? sectionURL(ctx, section, page) : pagedURL(baseURL(ctx) + (dynamicPath ? dynamicPath.path : '/new-post/'), page); try { const html = section ? await fetchSectionText(ctx, section, page) : await fetchText(ctx, url); const items = parseSectionCards(ctx, html); return { pageType: 'category', id: pageId, title: trendingTitle(title, html), style: 'media.posterGrid', itemAspectRatio: '2:3', page: page, nextPage: page + 1, hasMore: hasNextPage(html, page, items), items: items }; } catch (error) { return { pageType: 'category', id: pageId, title: title, style: 'media.posterGrid', itemAspectRatio: '2:3', page: page, hasMore: false, items: [diagnosticItem(title + '加载失败', error)] }; } } async function search(rawCtx) { const ctx = normalizeContext(rawCtx); const query = cleanText(firstNonEmpty(ctx.query, ctx.keyword, ctx.text)); const page = positiveInt(contextValue(ctx, ['page', 'pg', 'currentPage', 'pageNumber']), 1); if (!query) return { pageType: 'search', title: '搜索', keyword: '', page: page, hasMore: false, items: [] }; const url = searchURL(ctx, query, page); try { const html = await fetchText(ctx, url); const items = parseCards(ctx, html); return { pageType: 'search', title: '搜索:' + query, keyword: query, style: 'media.posterGrid', itemAspectRatio: '2:3', page: page, nextPage: page + 1, hasMore: hasNextPage(html, page, items), items: items }; } catch (error) { return { pageType: 'search', title: '搜索:' + query, keyword: query, page: page, hasMore: false, items: [diagnosticItem('搜索失败', error)] }; } } async function getDetail(rawCtx) { const ctx = normalizeContext(rawCtx); const detailUrl = detailURLFromContext(ctx); if (!detailUrl) throw new Error('JAVGG 详情参数无效'); const html = await fetchText(ctx, detailUrl); const title = cleanText(firstNonEmpty( metaContent(html, 'property', 'og:title'), firstMatch(html, /