Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added YouTube parser for thumbnail, content, snippet and other metadata. #297

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/bridges/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ const settingsBridge = {
ipcRenderer.invoke("set-menu", state)
},

getIconStatus: (): boolean => {
return ipcRenderer.sendSync("get-icon-status")
},
toggleIconStatus: () => {
ipcRenderer.send("toggle-icon-status")
},

getProxyStatus: (): boolean => {
return ipcRenderer.sendSync("get-proxy-status")
},
Expand Down
18 changes: 16 additions & 2 deletions src/components/settings/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import intl from "react-intl-universal"
import { urlTest, byteToMB, calculateItemSize, getSearchEngineName } from "../../scripts/utils"
import { ThemeSettings, SearchEngines } from "../../schema-types"
import { getThemeSettings, setThemeSettings, exportAll } from "../../scripts/settings"
import { Stack, Label, Toggle, TextField, DefaultButton, ChoiceGroup, IChoiceGroupOption, loadTheme, Dropdown, IDropdownOption, PrimaryButton } from "@fluentui/react"
import { Stack, Label, Toggle, TextField, DefaultButton, ChoiceGroup, IChoiceGroupOption, loadTheme, Dropdown, IDropdownOption, PrimaryButton, ToggleBase } from "@fluentui/react"
import DangerButton from "../utils/danger-button"

type AppTabProps = {
Expand All @@ -20,6 +20,7 @@ type AppTabState = {
itemSize: string
cacheSize: string
deleteIndex: string
iconStatus: boolean
}

class AppTab extends React.Component<AppTabProps, AppTabState> {
Expand All @@ -31,7 +32,8 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
themeSettings: getThemeSettings(),
itemSize: null,
cacheSize: null,
deleteIndex: null
deleteIndex: null,
iconStatus: window.settings.getIconStatus()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use a better variable name here and below for iconStatus that includes "youtube" to make this clearer?

}
this.getItemSize()
this.getCacheSize()
Expand Down Expand Up @@ -73,6 +75,11 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
this.props.setFetchInterval(item.key as number)
}

toggleIcon = () => {
window.settings.toggleIconStatus()
this.setState({ iconStatus: window.settings.getIconStatus() })
}

searchEngineOptions = (): IDropdownOption[] => [
SearchEngines.Google, SearchEngines.Bing, SearchEngines.Baidu, SearchEngines.DuckDuckGo
].map(engine => ({
Expand Down Expand Up @@ -183,6 +190,13 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
</Stack.Item>
</Stack>

<Toggle
label="Use custom icons when available"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All strings should be extracted for internationalization.

checked={this.state.iconStatus}
onText="Enabled"
offText="Disabled"
onChanged={this.toggleIcon} />

<Stack horizontal verticalAlign="baseline">
<Stack.Item grow>
<Label>{intl.get("app.enableProxy")}</Label>
Expand Down
3 changes: 3 additions & 0 deletions src/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ else if (process.platform === "win32") app.setAppUserModelId("me.hyliu.fluentrea

let restarting = false

// Usage of any user agent will promt youtube to redirect url, causing issues reading response
app.userAgentFallback = " "

function init() {
performUpdate(store)
nativeTheme.themeSource = store.get("theme", ThemeSettings.Default)
Expand Down
16 changes: 16 additions & 0 deletions src/main/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ ipcMain.handle("set-menu", (_, state: boolean) => {
store.set(MENU_STORE_KEY, state)
})


const ICON_STATUS_KEY = "customIcon"
function getIconStatus() {
return store.get(ICON_STATUS_KEY, true)
}
function toggleIconStatus() {
store.set(ICON_STATUS_KEY, !getIconStatus())
}

const PAC_STORE_KEY = "pac"
const PAC_STATUS_KEY = "pacOn"
function getProxyStatus() {
Expand All @@ -46,6 +55,13 @@ function setProxy(address = null) {
session.fromPartition("sandbox").setProxy(rules)
}
}
ipcMain.on("get-icon-status", (event) => {
event.returnValue = getIconStatus()
})
ipcMain.on("toggle-icon-status", () => {
toggleIconStatus()
})

ipcMain.on("get-proxy-status", (event) => {
event.returnValue = getProxyStatus()
})
Expand Down
1 change: 1 addition & 0 deletions src/schema-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,5 @@ export type SchemaTypes = {
filterType: number
listViewConfigs: ViewConfigs
useNeDB: boolean
customIcon: boolean
}
9 changes: 8 additions & 1 deletion src/scripts/models/item.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as db from "../db"
import lf from "lovefield"
import intl from "react-intl-universal"
import { domParser, htmlDecode, ActionStatus, AppThunk, platformCtrl } from "../utils"
import { domParser, htmlDecode, parseYouTubeContent, ActionStatus, AppThunk, platformCtrl } from "../utils"
import { RSSSource, updateSource, updateUnreadCounts } from "./source"
import { FeedActionTypes, INIT_FEED, LOAD_MORE, FilterType, initFeeds, dismissItems } from "./feed"
import Parser from "@yang991178/rss-parser"
Expand Down Expand Up @@ -75,6 +75,13 @@ export class RSSItem {
if (item.thumb && !item.thumb.startsWith("https://") && !item.thumb.startsWith("http://")) {
delete item.thumb
}
if (parsed.videoMeta) {
item.thumb = item.thumb ? item.thumb : parsed.videoMeta["media:thumbnail"][0].$.url
item.snippet = item.snippet ? item.snippet : parsed.videoMeta["media:description"][0] || ""
const views = parsed.videoMeta["media:community"][0]["media:statistics"][0].$.views
const likes = parsed.videoMeta["media:community"][0]["media:starRating"][0].$.count
item.content = parseYouTubeContent(item.link.replace("watch?v=","embed/"), views, likes, item.snippet)
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/scripts/models/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Parser from "@yang991178/rss-parser"
import intl from "react-intl-universal"
import * as db from "../db"
import lf from "lovefield"
import { fetchFavicon, ActionStatus, AppThunk, parseRSS } from "../utils"
import { fetchFavicon, fetchYTChannelIcon, ActionStatus, AppThunk, parseRSS } from "../utils"
import { RSSItem, insertItems, ItemActionTypes, FETCH_ITEMS, MARK_READ, MARK_UNREAD, MARK_ALL_READ } from "./item"
import { saveSettings } from "./app"
import { SourceRule } from "./rule"
Expand Down Expand Up @@ -322,8 +322,8 @@ export function updateFavicon(sids?: number[], force=false): AppThunk<Promise<vo
}
const promises = sids.map(async sid => {
const url = initSources[sid].url
let favicon = (await fetchFavicon(url)) || ""
const source = getState().sources[sid]
let favicon = (await fetchFavicon(url)) || ""
if (source && source.url === url && (force || source.iconurl === undefined)) {
source.iconurl = favicon
await dispatch(updateSource(source))
Expand Down
34 changes: 34 additions & 0 deletions src/scripts/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const rssParser = new Parser({
item: [
"thumb", "image", ["content:encoded", "fullContent"],
['media:content', 'mediaContent', {keepArray: true}],
['media:group', 'videoMeta', {keepArray: false}],
] as Parser.CustomFieldItem[]
}
})
Expand Down Expand Up @@ -73,8 +74,33 @@ export async function parseRSS(url: string) {

export const domParser = new DOMParser()

export async function fetchYTChannelIcon(url: string) {
let channelId = url.split("=")[1]
url = url.split("/").slice(0, 3).join("/")
let channelUrl = url + '/channel/' + channelId
let result = await fetch(channelUrl, { credentials: 'omit', })
if (result.ok) {
let html = await result.text()
let dom = domParser.parseFromString(html, "text/html")
let links = dom.getElementsByTagName("link")
for (let link of links) {
let rel = link.getAttribute("rel")
if (rel === "image_src" && link.hasAttribute("href")) {
let href = link.getAttribute("href")
return href.replace("=s900", "=s16")
}
}
}
}

export async function fetchFavicon(url: string) {
try {
const customIcon = window.settings.getIconStatus()
if (customIcon) {
if (Url.parse(url).host === "www.youtube.com" && url.includes("channel")) {
return fetchYTChannelIcon(url)
}
}
url = url.split("/").slice(0, 3).join("/")
let result = await fetch(url, { credentials: "omit" })
if (result.ok) {
Expand All @@ -88,6 +114,7 @@ export async function fetchFavicon(url: string) {
let parsedUrl = Url.parse(url)
if (href.startsWith("//")) return parsedUrl.protocol + href
else if (href.startsWith("/")) return url + href
else if (href.startsWith("favicon")) return url + '/' + href
else return href
}
}
Expand Down Expand Up @@ -121,6 +148,13 @@ export function htmlDecode(input: string) {
return doc.documentElement.textContent
}

export function parseYouTubeContent(url: string, views: string, likes: string, content: string){
const video = `<iframe width="560" height="315" src="${url}" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`
const views_likes = `<p style="float: left; color: #808080;"><em>${views} views</em></p><p style="float: right; color: #808080;"><em>${likes} likes</em></p>`
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same internationalization suggestion here.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gkd 👍

const _content = `<div style="word-break: break-all;>"<pre>${content.replace(/(?:\r\n|\r|\n)/g, '<br>')}</pre></div>`
return `${video}${views_likes}<br><br><br><hr />${_content}`
}

export const urlTest = (s: string) =>
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi.test(s)

Expand Down