mirror of
https://git.sr.ht/~cadence/cloudtube
synced 2026-03-02 02:31:35 +00:00
Implement video filters
This commit is contained in:
parent
aa953dc796
commit
db7ccabb3b
19 changed files with 790 additions and 9 deletions
160
api/filters.js
Normal file
160
api/filters.js
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
const constants = require("../utils/constants")
|
||||
const db = require("../utils/db")
|
||||
const {render} = require("pinski/plugins")
|
||||
const {getUser, getToken} = require("../utils/getuser")
|
||||
const validate = require("../utils/validate")
|
||||
const V = validate.V
|
||||
const {Matcher, PatternCompileError} = require("../utils/matcher")
|
||||
|
||||
const filterMaxLength = 160
|
||||
const regexpEnabledText = constants.server_setup.allow_regexp_filters ? "" : "not"
|
||||
|
||||
function getCategories(req) {
|
||||
const user = getUser(req)
|
||||
const filters = user.getFilters()
|
||||
|
||||
// Sort filters into categories for display. Titles are already sorted.
|
||||
const categories = {
|
||||
title: {name: "Title", filters: []},
|
||||
channel: {name: "Channel", filters: []}
|
||||
}
|
||||
for (const filter of filters) {
|
||||
if (filter.type === "title") {
|
||||
categories.title.filters.push(filter)
|
||||
} else { // filter.type is some kind of channel
|
||||
categories.channel.filters.push(filter)
|
||||
}
|
||||
}
|
||||
categories.channel.filters.sort((a, b) => {
|
||||
if (a.label && b.label) {
|
||||
if (a.label < b.label) return -1
|
||||
else if (a.label > b.label) return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
return categories
|
||||
}
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
route: "/filters", methods: ["GET"], code: async ({req, url}) => {
|
||||
const categories = getCategories(req)
|
||||
let referrer = url.searchParams.get("referrer") || null
|
||||
|
||||
let type = null
|
||||
let contents = ""
|
||||
let label = null
|
||||
if (url.searchParams.has("title")) {
|
||||
type = "title"
|
||||
contents = url.searchParams.get("title")
|
||||
} else if (url.searchParams.has("channel-id")) {
|
||||
type = "channel-id"
|
||||
contents = url.searchParams.get("channel-id")
|
||||
label = url.searchParams.get("label")
|
||||
}
|
||||
|
||||
return render(200, "pug/filters.pug", {categories, type, contents, label, referrer, filterMaxLength, regexpEnabledText})
|
||||
}
|
||||
},
|
||||
{
|
||||
route: "/filters", methods: ["POST"], upload: true, code: async ({req, body}) => {
|
||||
return new V()
|
||||
.with(validate.presetLoad({body}))
|
||||
.with(validate.presetURLParamsBody())
|
||||
.with(validate.presetEnsureParams(["filter-type", "new-filter"]))
|
||||
.check(state => {
|
||||
// Extract fields
|
||||
state.type = state.params.get("filter-type")
|
||||
state.contents = state.params.get("new-filter").slice(0, filterMaxLength)
|
||||
state.label = state.params.get("label")
|
||||
if (state.label) {
|
||||
state.label = state.label.slice(0, filterMaxLength)
|
||||
} else {
|
||||
state.label = null
|
||||
}
|
||||
state.referrer = state.params.get("referrer")
|
||||
// Check type
|
||||
return ["title", "channel-name", "channel-id"].includes(state.type)
|
||||
}, () => ({
|
||||
statusCode: 400,
|
||||
contentType: "application/json",
|
||||
content: {
|
||||
error: "type parameter is not in the list of filter types."
|
||||
}
|
||||
}))
|
||||
.check(state => {
|
||||
// If title, check that pattern compiles
|
||||
if (state.type === "title") {
|
||||
try {
|
||||
const matcher = new Matcher(state.contents)
|
||||
matcher.compilePattern()
|
||||
} catch (e) {
|
||||
if (e instanceof PatternCompileError) {
|
||||
state.compileError = e
|
||||
return false
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return true
|
||||
}, state => {
|
||||
const {type, contents, label, compileError} = state
|
||||
const categories = getCategories(req)
|
||||
return render(400, "pug/filters.pug", {categories, type, contents, label, compileError, filterMaxLength, regexpEnabledText})
|
||||
})
|
||||
.last(state => {
|
||||
const {type, contents, label} = state
|
||||
const responseHeaders = {
|
||||
Location: state.referrer || "/filters"
|
||||
}
|
||||
const token = getToken(req, responseHeaders)
|
||||
|
||||
db.prepare("INSERT INTO Filters (token, type, data, label) VALUES (?, ?, ?, ?)").run(token, type, contents, label)
|
||||
|
||||
return {
|
||||
statusCode: 303,
|
||||
headers: responseHeaders,
|
||||
contentType: "text/html",
|
||||
content: "Redirecting..."
|
||||
}
|
||||
})
|
||||
.go()
|
||||
}
|
||||
},
|
||||
{
|
||||
route: "/filters/delete", methods: ["POST"], upload: true, code: async ({req, body}) => {
|
||||
return new V()
|
||||
.with(validate.presetLoad({body}))
|
||||
.with(validate.presetURLParamsBody())
|
||||
.with(validate.presetEnsureParams(["delete-id"]))
|
||||
.check(state => {
|
||||
state.deleteID = +state.params.get("delete-id")
|
||||
return !!state.deleteID
|
||||
}, () => ({
|
||||
statusCode: 400,
|
||||
contentType: "application/json",
|
||||
content: {
|
||||
error: "delete-id parameter must be a number"
|
||||
}
|
||||
}))
|
||||
.last(state => {
|
||||
const {deleteID} = state
|
||||
const token = getToken(req)
|
||||
|
||||
// the IDs are unique, but can likely be guessed, so also use the token for actual authentication
|
||||
db.prepare("DELETE FROM Filters WHERE token = ? and id = ?").run(token, deleteID)
|
||||
|
||||
return {
|
||||
statusCode: 303,
|
||||
headers: {
|
||||
Location: "/filters"
|
||||
},
|
||||
contentType: "text/html",
|
||||
content: "Redirecting..."
|
||||
}
|
||||
})
|
||||
.go()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -7,10 +7,14 @@ module.exports = [
|
|||
{
|
||||
route: "/(?:search|results)", methods: ["GET"], code: async ({req, url}) => {
|
||||
const query = url.searchParams.get("q") || url.searchParams.get("search_query")
|
||||
const instanceOrigin = getUser(req).getSettingsOrDefaults().instance
|
||||
const user = getUser(req)
|
||||
const settings = user.getSettingsOrDefaults()
|
||||
const instanceOrigin = settings.instance
|
||||
|
||||
const fetchURL = new URL(`${instanceOrigin}/api/v1/search`)
|
||||
fetchURL.searchParams.set("q", query)
|
||||
const results = await request(fetchURL.toString()).then(res => res.json())
|
||||
|
||||
let results = await request(fetchURL.toString()).then(res => res.json())
|
||||
const error = results.error || results.message || results.code
|
||||
|
||||
if (error) throw new Error(`Instance said: ${error}`)
|
||||
|
|
@ -19,6 +23,9 @@ module.exports = [
|
|||
converters.normaliseVideoInfo(video)
|
||||
}
|
||||
|
||||
const filters = user.getFilters()
|
||||
results = converters.applyVideoFilters(results, filters).videos
|
||||
|
||||
return render(200, "pug/search.pug", {query, results, instanceOrigin})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
const {render} = require("pinski/plugins")
|
||||
const db = require("../utils/db")
|
||||
const {fetchChannelLatest} = require("../utils/youtube")
|
||||
const {getUser} = require("../utils/getuser")
|
||||
const {timeToPastText, rewriteVideoDescription} = require("../utils/converters")
|
||||
const {timeToPastText, rewriteVideoDescription, applyVideoFilters} = require("../utils/converters")
|
||||
const {refresher} = require("../background/feed-update")
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
route: `/subscriptions`, methods: ["GET"], code: async ({req}) => {
|
||||
route: `/subscriptions`, methods: ["GET"], code: async ({req, url}) => {
|
||||
const user = getUser(req)
|
||||
let hasSubscriptions = false
|
||||
let videos = []
|
||||
|
|
@ -36,10 +35,12 @@ module.exports = [
|
|||
return video
|
||||
})
|
||||
}
|
||||
const filters = user.getFilters()
|
||||
;({videos} = applyVideoFilters(videos, filters))
|
||||
}
|
||||
const settings = user.getSettingsOrDefaults()
|
||||
const instanceOrigin = settings.instance
|
||||
return render(200, "pug/subscriptions.pug", {settings, hasSubscriptions, videos, channels, refreshed, timeToPastText, instanceOrigin})
|
||||
return render(200, "pug/subscriptions.pug", {url, settings, hasSubscriptions, videos, channels, refreshed, timeToPastText, instanceOrigin})
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -145,6 +145,10 @@ module.exports = [
|
|||
converters.normaliseVideoInfo(rec)
|
||||
}
|
||||
|
||||
// filter list
|
||||
const {videos, filteredCount} = converters.applyVideoFilters(video.recommendedVideos, user.getFilters())
|
||||
video.recommendedVideos = videos
|
||||
|
||||
// get subscription data
|
||||
const subscribed = user.isSubscribed(video.authorId)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue