diff --git a/docs/new-media.md b/docs/new-media.md index 8558953846..4fefbc6689 100644 --- a/docs/new-media.md +++ b/docs/new-media.md @@ -540,15 +540,15 @@ Country ## GQ -### GQ 台湾 +### GQ Taiwan - + 分类 -| Fashion | Entertainment | Life | Gadget | Better Men | Video | Tag | -| ------- | ------------- | ---- | ------ | ---------- | ----- | --- | -| fashion | entertainment | life | gadget | bettermen | video | tag | +| Fashion | Shopping | Entertainment | Life | Gadget | Better Men | Video | Tag | +| ------- | ------------- | ------------- | ---- | ------ | ---------- | ----- | --- | +| fashion | gq-recommends | entertainment | life | gadget | bettermen | video | tag | 子分类 @@ -560,39 +560,33 @@ Fashion Entertainment -| 最新推薦 | 電影 | 娛樂 | 名人 | 美女 | 體育 | 特別報導 | -| ---- | ----- | ---------- | ----------- | ---- | ------ | ------- | -| | movie | popculture | celebrities | girl | sports | special | +| All topics | 電影 | 娛樂 | 名人 | 美女 | 體育 | 特別報導 | +| ---------- | ----- | ---------- | ----------- | ---- | ------ | ------- | +| | movie | popculture | celebrities | girl | sports | special | Life -| 最新推薦 | 美食 | 微醺 | 戶外生活 | 設計生活 | 風格幕後 | 特別報導 | -| ---- | ---- | ---- | ------- | ------ | ---------------- | ------- | -| | food | wine | outdoor | design | lifestyleinsider | special | +| All topics | 美食 | 微醺 | 戶外生活 | 設計生活 | 風格幕後 | 特別報導 | +| ---------- | ---- | ---- | ------- | ------ | ---------------- | ------- | +| | food | wine | outdoor | design | lifestyleinsider | special | Gadget -| 最新推薦 | 3C | 車 | 腕錶 | 特別報導 | -| ---- | -- | ---- | ----- | ------- | -| | 3c | auto | watch | special | +| All topics | 3C | 車 | 腕錶 | 特別報導 | +| ---------- | -- | ---- | ----- | ------- | +| | 3c | auto | watch | special | Better Men -| 最新推薦 | 保養健身 | 感情關係 | 性愛 | 特別報導 | -| ---- | --------- | ------------ | --- | ------- | -| | wellbeing | relationship | sex | special | - -Video - -| 最新推薦 | 名人 | 全球娛樂 | 玩家收藏 | 穿搭 | 生活 | -| ---- | ------ | ------------------- | ------- | ----- | ---- | -| | people | globalentertainment | collect | style | life | +| All topics | 保養健身 | 感情關係 | 性愛 | 特別報導 | +| ---------- | --------- | ------------ | --- | ------- | +| | wellbeing | relationship | sex | special | Tag -| 奧斯卡 | -| ------------------- | -| `the-oscars-奧斯卡金像獎` | +| 奧斯卡 | MOTY | +| ------------------- | ---- | +| `the-oscars-奧斯卡金像獎` | moty | diff --git a/lib/router.js b/lib/router.js index 8e7bd61cd7..f850f5030a 100644 --- a/lib/router.js +++ b/lib/router.js @@ -3179,7 +3179,7 @@ router.get('/1x/:category?', lazyloadRouteHandler('./routes/1x/index')); router.get('/jx3/:caty?', lazyloadRouteHandler('./routes/jx3/news')); // GQ -router.get('/gq/tw/:caty?/:subcaty?', lazyloadRouteHandler('./routes/gq/tw/index')); +// router.get('/gq/tw/:caty?/:subcaty?', lazyloadRouteHandler('./routes/gq/tw/index')); // 泉州市跨境电子商务协会 router.get('/qzcea/:caty?', lazyloadRouteHandler('./routes/qzcea/index')); diff --git a/lib/routes/gq/tw/index.js b/lib/routes/gq/tw/index.js deleted file mode 100644 index 76c99530b3..0000000000 --- a/lib/routes/gq/tw/index.js +++ /dev/null @@ -1,66 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); - -module.exports = async (ctx) => { - ctx.params.caty = ctx.params.caty || ''; - ctx.params.subcaty = ctx.params.subcaty || ''; - - const rootUrl = 'https://www.gq.com.tw'; - const currentUrl = `${rootUrl}${ctx.params.caty === '' ? '' : '/' + ctx.params.caty}${ctx.params.subcaty === '' ? '' : '/' + ctx.params.subcaty}`; - const response = await got({ - method: 'get', - url: currentUrl, - }); - - const $ = cheerio.load(response.data); - - $('a[data-test-id="NavElement"]').remove(); - $('a[data-test-id="Link"]').remove(); - $('a[data-test-id="FooterLink"]').remove(); - - const list = $('a[data-test-id]') - .slice(0, 10) - .map((_, item) => { - item = $(item); - return { - link: `${rootUrl}${item.attr('href')}`, - }; - }) - .get(); - - const items = await Promise.all( - list.map((item) => - ctx.cache.tryGet(item.link, async () => { - const detailResponse = await got({ - method: 'get', - url: item.link, - }); - const content = cheerio.load(detailResponse.data); - - item.title = content('h1').text(); - - const article = content('section[data-test-id="ArticleBodyContent"]').html(); - const gallery = content('section[data-test-id="GalleryImage"]').html(); - - if (ctx.params.caty === 'video') { - item.description = content('div[data-test-id="EmbedIframe"]').html() + content('p[data-test-id="DekText"]').parent().html(); - item.pubDate = new Date(detailResponse.data.match(/pubDate":"(.*?)","channel/)[1]).toUTCString(); - } else if (gallery !== null) { - item.description = gallery; - item.pubDate = new Date(detailResponse.data.match(/pubDate":"(.*?)","items/)[1]).toUTCString(); - } else { - item.description = article; - item.pubDate = new Date(detailResponse.data.match(/pubDate":"(.*?)","publishedRevisions/)[1]).toUTCString(); - } - - return item; - }) - ) - ); - - ctx.state.data = { - title: `${$('title').eq(0).text()} - GQ Taiwan`, - link: currentUrl, - item: items, - }; -}; diff --git a/lib/v2/gq/maintainer.js b/lib/v2/gq/maintainer.js new file mode 100644 index 0000000000..4423efadf9 --- /dev/null +++ b/lib/v2/gq/maintainer.js @@ -0,0 +1,3 @@ +module.exports = { + '/tw/:caty?/:subcaty?': ['nczitzk'], +}; diff --git a/lib/v2/gq/radar.js b/lib/v2/gq/radar.js new file mode 100644 index 0000000000..98028fb70f --- /dev/null +++ b/lib/v2/gq/radar.js @@ -0,0 +1,13 @@ +module.exports = { + 'gq.com.tw': { + _name: 'GQ Taiwan', + '.': [ + { + title: 'GQ Taiwan', + docs: 'https://docs.rsshub.app/new-media.html#gq', + source: ['/*path'], + target: '/gq/tw/:path', + }, + ], + }, +}; diff --git a/lib/v2/gq/router.js b/lib/v2/gq/router.js new file mode 100644 index 0000000000..3022fe20d5 --- /dev/null +++ b/lib/v2/gq/router.js @@ -0,0 +1,3 @@ +module.exports = (router) => { + router.get('/tw/:caty?/:subcaty?', require('./tw/index')); +}; diff --git a/lib/v2/gq/templates/embed-article.art b/lib/v2/gq/templates/embed-article.art new file mode 100644 index 0000000000..188ad1ff16 --- /dev/null +++ b/lib/v2/gq/templates/embed-article.art @@ -0,0 +1 @@ +{{ text }} diff --git a/lib/v2/gq/templates/embed-product.art b/lib/v2/gq/templates/embed-product.art new file mode 100644 index 0000000000..5f0ebf53fe --- /dev/null +++ b/lib/v2/gq/templates/embed-product.art @@ -0,0 +1,4 @@ +{{@ img }} +{{ productProps.dangerousHed }}
+{{ if productProps.offerRetailer }}{{ productProps.offerRetailer }} {{ /if }}{{ if productProps.multipleOffers }}{{ productProps.multipleOffers[0].price }}{{ /if }} +{{ if productProps.offerUrl }}
Buy It{{ /if }} diff --git a/lib/v2/gq/templates/img.art b/lib/v2/gq/templates/img.art new file mode 100644 index 0000000000..3019ef2764 --- /dev/null +++ b/lib/v2/gq/templates/img.art @@ -0,0 +1,4 @@ +
+ + {{ if alt }}
{{@ alt }}
{{ /if }} +
diff --git a/lib/v2/gq/templates/tw.art b/lib/v2/gq/templates/tw.art new file mode 100644 index 0000000000..fdbe4866bb --- /dev/null +++ b/lib/v2/gq/templates/tw.art @@ -0,0 +1,3 @@ +{{ if dangerousDek }}
{{@ dangerousDek }}
{{ /if }} +{{ if lede }}{{@ lede }}{{ /if }} +{{ if articleBody }}{{@ articleBody }}{{ /if }} diff --git a/lib/v2/gq/templates/videoObject.art b/lib/v2/gq/templates/videoObject.art new file mode 100644 index 0000000000..f78ab72d10 --- /dev/null +++ b/lib/v2/gq/templates/videoObject.art @@ -0,0 +1,6 @@ + +{{@ articleBody }} diff --git a/lib/v2/gq/templates/youtube.art b/lib/v2/gq/templates/youtube.art new file mode 100644 index 0000000000..0fd2a915b9 --- /dev/null +++ b/lib/v2/gq/templates/youtube.art @@ -0,0 +1,3 @@ +{{ if videoId }} + +{{ /if }} diff --git a/lib/v2/gq/tw/index.js b/lib/v2/gq/tw/index.js new file mode 100644 index 0000000000..252debac79 --- /dev/null +++ b/lib/v2/gq/tw/index.js @@ -0,0 +1,165 @@ +const got = require('@/utils/got'); +const cheerio = require('cheerio'); +const { parseDate } = require('@/utils/parse-date'); +const md = require('markdown-it')({ + linkify: true, +}); +const { art } = require('@/utils/render'); +const path = require('path'); + +const baseUrl = 'https://www.gq.com.tw'; +const parsePreloadedStateJSON = ($) => + JSON.parse( + $('script[type="text/javascript"]') + .text() + .match(/window\.__PRELOADED_STATE__ = ({.*?});/)[1] + ); +const largestImage = (sources, id) => { + if (!id) { + let maxWidth = 0; + let maxUrl = ''; + Object.values(sources).forEach((size) => { + if (size.width > maxWidth) { + maxWidth = size.width; + maxUrl = size.url; + } + }); + return maxUrl; + } + const url = sources[Object.keys(sources)[0]].url; + const filename = url.substring(url.lastIndexOf('/') + 1); + return `https://media.gq.com.tw/photos/${id}/${filename}`; +}; + +module.exports = async (ctx) => { + const { caty, subcaty } = ctx.params; + const link = `${baseUrl}${caty ? `/${caty}` : ''}${subcaty ? `/${subcaty}` : ''}`; + const { data: response } = await got(link); + const $ = cheerio.load(response); + const { transformed } = parsePreloadedStateJSON($); + + const list = transformed.bundle.containers + .filter((item) => item.items) + .map((item) => + item.items.map((item) => ({ + title: item.source?.hed || item.dangerousHed, + description: item.source?.dek || item.dangerousDek, + link: (item.url.startsWith('http') ? item.url : `${baseUrl}${item.url}`).split('#intcid')[0], + pubDate: parseDate(item.pubDate), + author: item.contributors.author.items.map((item) => item.name).join(', '), + })) + ) + .flat(); + + const items = await Promise.all( + list.map((item) => + ctx.cache.tryGet(item.link, async () => { + const { data: response } = await got(item.link); + const $ = cheerio.load(response); + const data = JSON.parse($('script[type="application/ld+json"]').first().text()); + const { transformed } = parsePreloadedStateJSON($); + const VideoObject = data['@type'] === 'VideoObject' ? true : false; + const lede = !VideoObject ? transformed.article.headerProps.lede : null; + + let articleBody = ''; + if (!VideoObject) { + // typical article + // articleBody inludes non standard markdown syntax + articleBody = cheerio.load(md.render(data.articleBody.replace(/\{: target="_blank"\}/g, '')).replace(/\{: #link\}/g, ''), null, false); + articleBody('a').each((_, item) => { + item = $(item); + if (item.text().startsWith('#video:')) { + if (item.text().match(/youtube/)) { + const videoId = item.text().match(/.*https:\/\/www\.youtube\.com\/embed\/(.*)\s/)[1]; + item.replaceWith(art(path.join(__dirname, '../templates/youtube.art'), { videoId })); + } + } + if (item.text().startsWith('![#image:')) { + const imageId = item.text().match(/!\[#image: \/photos\/(.*?)\]/)[1]; + const imgInfo = transformed.article.lightboxImages.find((item) => item.id === imageId); + item.html( + art(path.join(__dirname, '../templates/img.art'), { + src: largestImage(imgInfo.sources, imageId), + alt: imgInfo.dangerousCaption, + }) + ); + } + }); + articleBody('p').each((_, item) => { + item = $(item); + if (item.text().startsWith('+++')) { + item.remove(); + } + if (item.text().startsWith('[#image:')) { + const imageId = item.text().match(/\[#image: \/photos\/(.*?)\]/)[1]; + const imgInfo = transformed.article.lightboxImages.find((item) => item.id === imageId); + item.replaceWith( + art(path.join(__dirname, '../templates/img.art'), { + src: largestImage(imgInfo.sources, imageId), + alt: imgInfo.dangerousCaption, + }) + ); + } + if (item.text().startsWith('[#article:')) { + const articleId = item.text().match(/\[#article: \/articles\/(.*?)\]/)[1]; + const articleProps = transformed.article.body.filter((i) => i[0] === 'inline-embed').find((i) => i[1].ref === articleId)[1].props; + item.replaceWith( + art(path.join(__dirname, '../templates/embed-article.art'), { + url: `${baseUrl}${articleProps.url}`, + text: articleProps.dangerousHed, + }) + ); + } + if (item.text().startsWith('[#product:')) { + const productId = item.text().match(/\[#product: \/products\/(.*?)\]/)[1]; + const productProps = transformed.article.body.filter((i) => i[0] === 'inline-embed').find((i) => i[1].ref === productId)[1].props; + item.replaceWith( + art(path.join(__dirname, '../templates/embed-product.art'), { + img: art(path.join(__dirname, '../templates/img.art'), { + src: largestImage(productProps.image.sources), + }), + productProps, + }) + ); + } + if (item.text().startsWith('[#instagram:')) { + const instagramHref = item.text().match(/\[#instagram: (.*?)\]/)[1]; + item.replaceWith( + art(path.join(__dirname, '../templates/embed-article.art'), { + url: instagramHref, + text: instagramHref, + }) + ); + } + }); + item.description = art(path.join(__dirname, '../templates/tw.art'), { + dangerousDek: transformed.article.headerProps.dangerousDek, // quote + lede: art(path.join(__dirname, '../templates/img.art'), { + // header image + src: largestImage(lede.sources, lede.id), + alt: lede.caption, + }), + articleBody: articleBody.html(), + }); + } else { + // is VideoObject + item.description = art(path.join(__dirname, '../templates/videoObject.art'), { + poster: transformed.video.metaImageUrl, + sources: transformed.video.sources, + articleBody: data.description, + }); + } + + return item; + }) + ) + ); + + ctx.state.data = { + title: `${transformed.coreDataLayer.content.contentTitle} | ${transformed.coreDataLayer.content.brand}`, + link, + image: `${baseUrl}${transformed.logo.sources.sm.url}`, + item: items, + allowEmpty: true, + }; +};