Update scripts

This commit is contained in:
freearhey 2025-03-18 08:32:46 +03:00
parent 77680e2dc9
commit 075c53143e
14 changed files with 520 additions and 132 deletions

View file

@ -21,7 +21,10 @@ const opts = {
replaced_by: nullable, replaced_by: nullable,
website: nullable, website: nullable,
logo: nullable, logo: nullable,
countries: listParser countries: listParser,
timezones: listParser,
is_main: boolParser,
video_format: nullable
} }
} }

View file

@ -1,17 +0,0 @@
export class IDCreator {
create(name: string, country: string): string {
const slug = normalize(name)
const code = country.toLowerCase()
return `${slug}.${code}`
}
}
function normalize(name: string) {
return name
.replace(/^@/gi, 'At')
.replace(/^&/i, 'And')
.replace(/\+/gi, 'Plus')
.replace(/\s-(\d)/gi, ' Minus$1')
.replace(/[^a-z\d]+/gi, '')
}

View file

@ -2,6 +2,5 @@ export * from './csv'
export * from './issueParser' export * from './issueParser'
export * from './issueLoader' export * from './issueLoader'
export * from './csvParser' export * from './csvParser'
export * from './idCreator'
export * from './issueData' export * from './issueData'
export * from './issue' export * from './issue'

View file

@ -9,32 +9,15 @@ const CustomOctokit = Octokit.plugin(paginateRest, restEndpointMethods)
const octokit = new CustomOctokit() const octokit = new CustomOctokit()
export class IssueLoader { export class IssueLoader {
async load({ labels }: { labels: string[] | string }) { async load(props?: { labels: string[] | string }) {
labels = Array.isArray(labels) ? labels.join(',') : labels let labels = ''
if (props && props.labels) {
labels = Array.isArray(props.labels) ? props.labels.join(',') : props.labels
}
let issues: object[] = [] let issues: object[] = []
if (TESTING) { if (TESTING) {
switch (labels) { issues = (await import('../../tests/__data__/input/update/issues.js')).default
case 'channels:add,approved':
issues = (await import('../../tests/__data__/input/issues/channels_add_approved.js'))
.default
break
case 'channels:edit,approved':
issues = (await import('../../tests/__data__/input/issues/channels_edit_approved.js'))
.default
break
case 'channels:remove,approved':
issues = (await import('../../tests/__data__/input/issues/channels_remove_approved.js'))
.default
break
case 'blocklist:add,approved':
issues = (await import('../../tests/__data__/input/issues/blocklist_add_approved.js'))
.default
break
case 'blocklist:remove,approved':
issues = (await import('../../tests/__data__/input/issues/blocklist_remove_approved.js'))
.default
break
}
} else { } else {
issues = await octokit.paginate(octokit.rest.issues.listForRepo, { issues = await octokit.paginate(octokit.rest.issues.listForRepo, {
owner: OWNER, owner: OWNER,

View file

@ -3,40 +3,30 @@ import { IssueData, Issue } from '../core'
const FIELDS = new Dictionary({ const FIELDS = new Dictionary({
'Channel ID': 'channel_id', 'Channel ID': 'channel_id',
'Channel ID (required)': 'channel_id', 'Channel Name': 'channel_name',
'Channel ID (optional)': 'channel_id', 'Feed Name': 'feed_name',
'Channel Name': 'name', 'Feed ID': 'feed_id',
'Main Feed': 'is_main',
'Alternative Names': 'alt_names', 'Alternative Names': 'alt_names',
'Alternative Names (optional)': 'alt_names',
Network: 'network', Network: 'network',
'Network (optional)': 'network',
Owners: 'owners', Owners: 'owners',
'Owners (optional)': 'owners',
Country: 'country', Country: 'country',
Subdivision: 'subdivision', Subdivision: 'subdivision',
'Subdivision (optional)': 'subdivision',
City: 'city', City: 'city',
'City (optional)': 'city',
'Broadcast Area': 'broadcast_area', 'Broadcast Area': 'broadcast_area',
Timezones: 'timezones',
Format: 'video_format',
Languages: 'languages', Languages: 'languages',
Categories: 'categories', Categories: 'categories',
'Categories (optional)': 'categories',
NSFW: 'is_nsfw', NSFW: 'is_nsfw',
Launched: 'launched', Launched: 'launched',
'Launched (optional)': 'launched',
Closed: 'closed', Closed: 'closed',
'Closed (optional)': 'closed',
'Replaced By': 'replaced_by', 'Replaced By': 'replaced_by',
'Replaced By (optional)': 'replaced_by',
Website: 'website', Website: 'website',
'Website (optional)': 'website',
Logo: 'logo', Logo: 'logo',
Reason: 'reason', Reason: 'reason',
Notes: 'notes', Notes: 'notes',
'Notes (optional)': 'notes', Reference: 'ref'
Reference: 'ref',
'Reference (optional)': 'ref',
'Reference (required)': 'ref'
}) })
export class IssueParser { export class IssueParser {
@ -46,7 +36,7 @@ export class IssueParser {
const data = new Dictionary() const data = new Dictionary()
fields.forEach((field: string) => { fields.forEach((field: string) => {
let [_label, , _value] = field.split(/\r?\n/) let [_label, , _value] = field.split(/\r?\n/)
_label = _label ? _label.trim() : '' _label = _label ? _label.replace(/ \(optional\)| \(required\)/, '').trim() : ''
_value = _value ? _value.trim() : '' _value = _value ? _value.trim() : ''
if (!_label || !_value) return data if (!_label || !_value) return data

View file

@ -1,36 +1,49 @@
import { CSV, IssueLoader, CSVParser, IDCreator, Issue, IssueData } from '../core' import { CSV, IssueLoader, CSVParser, Issue, IssueData } from '../core'
import { Channel, Blocked } from '../models' import { Channel, Blocked, Feed } from '../models'
import { DATA_DIR } from '../constants' import { DATA_DIR } from '../constants'
import { Storage, Collection } from '@freearhey/core' import { Storage, Collection } from '@freearhey/core'
import { createChannelId, createFeedId } from '../utils'
let blocklist = new Collection() let blocklist = new Collection()
let channels = new Collection() let channels = new Collection()
let feeds = new Collection()
let issues = new Collection()
const processedIssues = new Collection() const processedIssues = new Collection()
async function main() { async function main() {
const idCreator = new IDCreator()
const dataStorage = new Storage(DATA_DIR) const dataStorage = new Storage(DATA_DIR)
const parser = new CSVParser() const parser = new CSVParser()
const _channels = await dataStorage.load('channels.csv')
channels = (await parser.parse(_channels)).map(data => new Channel(data))
const _blocklist = await dataStorage.load('blocklist.csv')
blocklist = (await parser.parse(_blocklist)).map(data => new Blocked(data))
const loader = new IssueLoader() const loader = new IssueLoader()
await removeChannels({ loader }) issues = await loader.load()
await editChannels({ loader, idCreator })
await addChannels({ loader, idCreator })
await blockChannels({ loader })
await unblockChannels({ loader })
channels = sortBy(channels, 'id') const channelsCSV = await dataStorage.load('channels.csv')
channels = (await parser.parse(channelsCSV)).map(data => new Channel(data))
const feedsCSV = await dataStorage.load('feeds.csv')
feeds = (await parser.parse(feedsCSV)).map(data => new Feed(data))
const blocklistCSV = await dataStorage.load('blocklist.csv')
blocklist = (await parser.parse(blocklistCSV)).map(data => new Blocked(data))
await removeFeeds()
await removeChannels()
await editFeeds()
await editChannels()
await addFeeds()
await addChannels()
await blockChannels()
await unblockChannels()
channels = channels.sortBy(channel => channel.id.toLowerCase())
const channelsOutput = new CSV({ items: channels }).toString() const channelsOutput = new CSV({ items: channels }).toString()
await dataStorage.save('channels.csv', channelsOutput) await dataStorage.save('channels.csv', channelsOutput)
blocklist = sortBy(blocklist, 'channel') feeds = feeds.sortBy(feed => `${feed.channel}@${feed.id}`.toLowerCase())
const feedsOutput = new CSV({ items: feeds }).toString()
await dataStorage.save('feeds.csv', feedsOutput)
blocklist = blocklist.sortBy(blocked => blocked.channel.toLowerCase())
const blocklistOutput = new CSV({ items: blocklist }).toString() const blocklistOutput = new CSV({ items: blocklist }).toString()
await dataStorage.save('blocklist.csv', blocklistOutput) await dataStorage.save('blocklist.csv', blocklistOutput)
@ -40,21 +53,139 @@ async function main() {
main() main()
function sortBy(channels: Collection, key: string) { async function removeFeeds() {
const items = channels.all().sort((a, b) => { const requests = issues.filter(
const normA = a[key].toLowerCase() issue => issue.labels.includes('feeds:remove') && issue.labels.includes('approved')
const normB = b[key].toLowerCase() )
if (normA < normB) return -1
if (normA > normB) return 1
return 0
})
return new Collection(items) requests.forEach((issue: Issue) => {
if (issue.data.missing('channel_id') || issue.data.missing('feed_id')) return
const found = feeds.first(
(feed: Feed) =>
feed.channel === issue.data.getString('channel_id') &&
feed.id === issue.data.getString('feed_id')
)
if (!found) return
feeds.remove((feed: Feed) => feed.channel === found.channel && feed.id === found.id)
onFeedRemoval(found.channel, found.id)
processedIssues.push(issue)
})
} }
async function removeChannels({ loader }: { loader: IssueLoader }) { async function editFeeds() {
const issues = await loader.load({ labels: ['channels:remove,approved'] }) const requests = issues.filter(
issues.forEach((issue: Issue) => { issue => issue.labels.includes('feeds:edit') && issue.labels.includes('approved')
)
requests.forEach((issue: Issue) => {
const data: IssueData = issue.data
if (data.missing('channel_id') || data.missing('feed_id')) return
const found: Feed = feeds.first(
(feed: Feed) =>
feed.channel === data.getString('channel_id') && feed.id === data.getString('feed_id')
)
if (!found) return
let channelId: string | undefined = found.channel
let feedId: string | undefined = found.id
if (data.has('feed_name')) {
const name = data.getString('feed_name') || found.name
if (name) {
feedId = createFeedId(name)
if (feedId) onFeedIdChange(found.channel, found.id, feedId)
}
}
if (data.has('is_main')) {
const isMain = data.getBoolean('is_main') || false
if (isMain) onFeedNewMain(channelId, feedId)
}
if (!feedId || !channelId) return
const updated = new Feed({
channel: channelId,
id: feedId,
name: data.getString('feed_name'),
is_main: data.getBoolean('is_main'),
broadcast_area: data.getArray('broadcast_area'),
timezones: data.getArray('timezones'),
languages: data.getArray('languages'),
video_format: data.getString('video_format'),
launched: data.getString('launched'),
closed: data.getString('closed'),
replaced_by: data.getString('replaced_by')
})
found.merge(updated)
processedIssues.push(issue)
})
}
async function addFeeds() {
const requests = issues.filter(
issue => issue.labels.includes('feeds:add') && issue.labels.includes('approved')
)
requests.forEach((issue: Issue) => {
const data: IssueData = issue.data
if (
data.missing('channel_id') ||
data.missing('feed_name') ||
data.missing('is_main') ||
data.missing('broadcast_area') ||
data.missing('timezones') ||
data.missing('languages') ||
data.missing('video_format')
)
return
const channelId = data.getString('channel_id')
const feedName = data.getString('feed_name') || 'SD'
const feedId = createFeedId(feedName)
if (!channelId || !feedId) return
const found: Feed = feeds.first(
(feed: Feed) => feed.channel === channelId && feed.id === feedId
)
if (found) return
const isMain = data.getBoolean('is_main') || false
if (isMain) onFeedNewMain(channelId, feedId)
feeds.push(
new Feed({
channel: channelId,
id: feedId,
name: feedName,
is_main: data.getBoolean('is_main'),
broadcast_area: data.getArray('broadcast_area'),
timezones: data.getArray('timezones'),
languages: data.getArray('languages'),
video_format: data.getString('video_format'),
launched: data.getString('launched'),
closed: data.getString('closed'),
replaced_by: data.getString('replaced_by')
})
)
processedIssues.push(issue)
})
}
async function removeChannels() {
const requests = issues.filter(
issue => issue.labels.includes('channels:remove') && issue.labels.includes('approved')
)
requests.forEach((issue: Issue) => {
if (issue.data.missing('channel_id')) return if (issue.data.missing('channel_id')) return
const found = channels.first( const found = channels.first(
@ -64,13 +195,18 @@ async function removeChannels({ loader }: { loader: IssueLoader }) {
channels.remove((channel: Channel) => channel.id === found.id) channels.remove((channel: Channel) => channel.id === found.id)
onChannelRemoval(found.id)
processedIssues.push(issue) processedIssues.push(issue)
}) })
} }
async function editChannels({ loader, idCreator }: { loader: IssueLoader; idCreator: IDCreator }) { async function editChannels() {
const issues = await loader.load({ labels: ['channels:edit,approved'] }) const requests = issues.filter(
issues.forEach((issue: Issue) => { issue => issue.labels.includes('channels:edit') && issue.labels.includes('approved')
)
requests.forEach((issue: Issue) => {
const data: IssueData = issue.data const data: IssueData = issue.data
if (data.missing('channel_id')) return if (data.missing('channel_id')) return
@ -79,20 +215,21 @@ async function editChannels({ loader, idCreator }: { loader: IssueLoader; idCrea
) )
if (!found) return if (!found) return
let channelId = found.id let channelId: string | undefined = found.id
if (data.has('name') || data.has('country')) { if (data.has('channel_name') || data.has('country')) {
const name = data.getString('name') || found.name const name = data.getString('channel_name') || found.name
const country = data.getString('country') || found.country const country = data.getString('country') || found.country
if (name && country) { if (name && country) {
channelId = idCreator.create(name, country) channelId = createChannelId(name, country)
updateBlocklistId(found.id, channelId) if (channelId) onChannelIdChange(found.id, channelId)
updateChannelReplacedBy(found.id, channelId)
} }
} }
if (!channelId) return
const updated = new Channel({ const updated = new Channel({
id: channelId, id: channelId,
name: data.getString('name'), name: data.getString('channel_name'),
alt_names: data.getArray('alt_names'), alt_names: data.getArray('alt_names'),
network: data.getString('network'), network: data.getString('network'),
owners: data.getArray('owners'), owners: data.getArray('owners'),
@ -116,16 +253,29 @@ async function editChannels({ loader, idCreator }: { loader: IssueLoader; idCrea
}) })
} }
async function addChannels({ loader, idCreator }: { loader: IssueLoader; idCreator: IDCreator }) { async function addChannels() {
const issues = await loader.load({ labels: ['channels:add,approved'] }) const requests = issues.filter(
issues.forEach((issue: Issue) => { issue => issue.labels.includes('channels:add') && issue.labels.includes('approved')
)
requests.forEach((issue: Issue) => {
const data: IssueData = issue.data const data: IssueData = issue.data
const name = data.getString('name')
const country = data.getString('country')
if (!name || !country) return if (
data.missing('channel_name') ||
data.missing('country') ||
data.missing('is_nsfw') ||
data.missing('logo') ||
data.missing('feed_name') ||
data.missing('broadcast_area') ||
data.missing('timezones') ||
data.missing('languages') ||
data.missing('video_format')
)
return
const channelId = idCreator.create(name, country) const channelId = createChannelId(data.getString('channel_name'), data.getString('country'))
if (!channelId) return
const found: Channel = channels.first((channel: Channel) => channel.id === channelId) const found: Channel = channels.first((channel: Channel) => channel.id === channelId)
if (found) return if (found) return
@ -133,7 +283,7 @@ async function addChannels({ loader, idCreator }: { loader: IssueLoader; idCreat
channels.push( channels.push(
new Channel({ new Channel({
id: channelId, id: channelId,
name: data.getString('name'), name: data.getString('channel_name'),
alt_names: data.getArray('alt_names'), alt_names: data.getArray('alt_names'),
network: data.getString('network'), network: data.getString('network'),
owners: data.getArray('owners'), owners: data.getArray('owners'),
@ -152,13 +302,34 @@ async function addChannels({ loader, idCreator }: { loader: IssueLoader; idCreat
}) })
) )
const feedName = data.getString('feed_name') || 'SD'
feeds.push(
new Feed({
channel: channelId,
id: createFeedId(feedName),
name: feedName,
is_main: true,
broadcast_area: data.getArray('broadcast_area'),
timezones: data.getArray('timezones'),
languages: data.getArray('languages'),
video_format: data.getString('video_format'),
launched: data.getString('launched'),
closed: data.getString('closed'),
replaced_by: data.getString('replaced_by')
})
)
processedIssues.push(issue) processedIssues.push(issue)
}) })
} }
async function unblockChannels({ loader }: { loader: IssueLoader }) { async function unblockChannels() {
const issues = await loader.load({ labels: ['blocklist:remove,approved'] }) const requests = issues.filter(
issues.forEach((issue: Issue) => { issue => issue.labels.includes('blocklist:remove') && issue.labels.includes('approved')
)
requests.forEach((issue: Issue) => {
const data = issue.data const data = issue.data
if (data.missing('channel_id')) return if (data.missing('channel_id')) return
@ -173,9 +344,12 @@ async function unblockChannels({ loader }: { loader: IssueLoader }) {
}) })
} }
async function blockChannels({ loader }: { loader: IssueLoader }) { async function blockChannels() {
const issues = await loader.load({ labels: ['blocklist:add,approved'] }) const requests = issues.filter(
issues.forEach((issue: Issue) => { issue => issue.labels.includes('blocklist:add') && issue.labels.includes('approved')
)
requests.forEach((issue: Issue) => {
const data = issue.data const data = issue.data
if (data.missing('channel_id')) return if (data.missing('channel_id')) return
@ -201,22 +375,77 @@ async function blockChannels({ loader }: { loader: IssueLoader }) {
}) })
} }
function updateBlocklistId(oldId: string, newId: string) { function onFeedIdChange(channelId: string, feedId: string, newFeedId: string) {
const filtered: Collection = blocklist.filter((blocked: Blocked) => blocked.channel === oldId) channels.forEach((channel: Channel) => {
if (filtered.isEmpty()) return if (channel.replaced_by && channel.replaced_by === `${channelId}@${feedId}`) {
channel.replaced_by = `${channelId}@${newFeedId}`
}
})
filtered.forEach(item => { feeds.forEach((feed: Feed) => {
item.channel = newId if (feed.replaced_by && feed.replaced_by === `${channelId}@${feedId}`) {
feed.replaced_by = `${channelId}@${newFeedId}`
}
}) })
} }
function updateChannelReplacedBy(channelId: string, newReplacedBy: string) { function onFeedNewMain(channelId: string, feedId: string) {
const filtered: Collection = channels.filter( feeds.forEach((feed: Feed) => {
(channel: Channel) => channel.replaced_by === channelId if (feed.channel === channelId && feed.id !== feedId && feed.is_main === true) {
) feed.is_main = false
if (filtered.isEmpty()) return }
})
filtered.forEach(item => { }
item.replaced_by = newReplacedBy
function onFeedRemoval(channelId: string, feedId: string) {
channels.forEach((channel: Channel) => {
if (channel.replaced_by && channel.replaced_by === `${channelId}@${feedId}`) {
channel.replaced_by = ''
}
})
feeds.forEach((feed: Feed) => {
if (feed.replaced_by && feed.replaced_by === `${channelId}@${feedId}`) {
feed.replaced_by = ''
}
})
}
function onChannelIdChange(channelId: string, newChannelId: string) {
channels.forEach((channel: Channel) => {
if (channel.replaced_by && channel.replaced_by.includes(channelId)) {
channel.replaced_by = channel.replaced_by.replace(channelId, newChannelId)
}
})
feeds.forEach((feed: Feed) => {
if (feed.channel === channelId) {
feed.channel = newChannelId
}
if (feed.replaced_by && feed.replaced_by.includes(channelId)) {
feed.replaced_by = feed.replaced_by.replace(channelId, newChannelId)
}
})
blocklist.forEach((blocked: Blocked) => {
if (blocked.channel === channelId) {
blocked.channel = newChannelId
}
})
}
function onChannelRemoval(channelId: string) {
channels.forEach((channel: Channel) => {
if (channel.replaced_by && channel.replaced_by.includes(channelId)) {
channel.replaced_by = ''
}
})
feeds.remove((feed: Feed) => feed.channel === channelId)
feeds.forEach((feed: Feed) => {
if (feed.replaced_by && feed.replaced_by.includes(channelId)) {
feed.replaced_by = ''
}
}) })
} }

View file

@ -3,8 +3,9 @@ import { DATA_DIR } from '../constants'
import schemesData from '../schemes' import schemesData from '../schemes'
import { program } from 'commander' import { program } from 'commander'
import Joi from 'joi' import Joi from 'joi'
import { CSVParser, IDCreator } from '../core' import { CSVParser } from '../core'
import chalk from 'chalk' import chalk from 'chalk'
import { createChannelId } from '../utils'
program.argument('[filepath]', 'Path to file to validate').parse(process.argv) program.argument('[filepath]', 'Path to file to validate').parse(process.argv)
@ -42,11 +43,15 @@ async function main() {
let grouped let grouped
switch (filename) { switch (filename) {
case 'feeds':
grouped = data.keyBy(item => item.channel + item.id)
break
case 'blocklist': case 'blocklist':
grouped = data.keyBy(item => item.channel + item.ref) grouped = data.keyBy(item => item.channel + item.ref)
break break
case 'categories': case 'categories':
case 'channels': case 'channels':
case 'timezones':
grouped = data.keyBy(item => item.id) grouped = data.keyBy(item => item.id)
break break
default: default:
@ -91,6 +96,17 @@ async function main() {
) )
} }
break break
case 'feeds':
fileErrors = fileErrors.concat(findDuplicatesBy(rowsCopy, ['channel', 'id']))
fileErrors = fileErrors.concat(validateMainFeeds(rowsCopy))
for (const [i, row] of rowsCopy.entries()) {
fileErrors = fileErrors.concat(validateChannel(row.channel, i))
fileErrors = fileErrors.concat(validateTimezones(row, i))
fileErrors = fileErrors.concat(
checkValue(i, row, 'id', 'replaced_by', buffer.get('channels'))
)
}
break
case 'blocklist': case 'blocklist':
fileErrors = fileErrors.concat(findDuplicatesBy(rowsCopy, ['channel', 'ref'])) fileErrors = fileErrors.concat(findDuplicatesBy(rowsCopy, ['channel', 'ref']))
for (const [i, row] of rowsCopy.entries()) { for (const [i, row] of rowsCopy.entries()) {
@ -201,7 +217,7 @@ function findDuplicatesBy(rows: { [key: string]: string }[], keys: string[]) {
const buffer = new Dictionary() const buffer = new Dictionary()
rows.forEach((row, i) => { rows.forEach((row, i) => {
const normId = keys.map(key => row[key].toLowerCase()).join() const normId = keys.map(key => row[key].toString().toLowerCase()).join()
if (buffer.has(normId)) { if (buffer.has(normId)) {
const fieldsList = keys.map(key => `${key} "${row[key]}"`).join(' and ') const fieldsList = keys.map(key => `${key} "${row[key]}"`).join(' and ')
errors.push({ errors.push({
@ -216,10 +232,31 @@ function findDuplicatesBy(rows: { [key: string]: string }[], keys: string[]) {
return errors return errors
} }
function validateMainFeeds(rows: { [key: string]: string }[]) {
const errors = new Collection()
const buffer = new Dictionary()
rows.forEach((row, i) => {
const normId = `${row.channel}${row.is_main}`
if (buffer.has(normId)) {
errors.push({
line: i + 2,
message: `entry with the channel "${row.channel}" and is_main "true" already exists`
})
}
if (row.is_main) {
buffer.set(normId, true)
}
})
return errors
}
function validateChannelId(row: { [key: string]: string }, i: number) { function validateChannelId(row: { [key: string]: string }, i: number) {
const errors = new Collection() const errors = new Collection()
const expectedId = new IDCreator().create(row.name, row.country) const expectedId = createChannelId(row.name, row.country)
if (expectedId !== row.id) { if (expectedId !== row.id) {
errors.push({ errors.push({
@ -254,6 +291,22 @@ function validateChannelBroadcastArea(row: { [key: string]: string[] }, i: numbe
return errors return errors
} }
function validateTimezones(row: { [key: string]: string[] }, i: number) {
const errors = new Collection()
const timezones = buffer.get('timezones')
row.timezones.forEach((timezone: string) => {
if (timezones.missing(timezone)) {
errors.push({
line: i + 2,
message: `"${row.channel}@${row.id}" has the wrong timezone "${timezone}"`
})
}
})
return errors
}
function handleError(message: string) { function handleError(message: string) {
logger.error(chalk.red(message)) logger.error(chalk.red(message))
process.exit(1) process.exit(1)

67
scripts/models/feed.ts Normal file
View file

@ -0,0 +1,67 @@
type FeedProps = {
channel: string
id: string
name?: string
is_main?: boolean
broadcast_area?: string[]
timezones?: string[]
languages?: string[]
video_format?: string
launched?: string
closed?: string
replaced_by?: string
}
export class Feed {
channel: string
id: string
name?: string
is_main?: boolean
broadcast_area: string[]
timezones: string[]
languages: string[]
video_format?: string
launched?: string
closed?: string
replaced_by?: string
constructor({
channel,
id,
name,
is_main,
broadcast_area,
timezones,
languages,
video_format,
launched,
closed,
replaced_by
}: FeedProps) {
this.channel = channel
this.id = id
this.name = name
this.is_main = is_main
this.broadcast_area = broadcast_area || []
this.timezones = timezones || []
this.languages = languages || []
this.video_format = video_format
this.launched = launched
this.closed = closed
this.replaced_by = replaced_by
}
data() {
const { ...object } = this
return object
}
merge(feed: Feed) {
const data: { [key: string]: string | string[] | boolean | undefined } = feed.data()
for (const prop in data) {
if (data[prop] === undefined) continue
this[prop] = data[prop]
}
}
}

View file

@ -1,2 +1,3 @@
export * from './channel' export * from './channel'
export * from './blocked' export * from './blocked'
export * from './feed'

View file

@ -46,7 +46,7 @@ export default {
launched: Joi.date().format('YYYY-MM-DD').raw().allow(null), launched: Joi.date().format('YYYY-MM-DD').raw().allow(null),
closed: Joi.date().format('YYYY-MM-DD').raw().allow(null).greater(Joi.ref('launched')), closed: Joi.date().format('YYYY-MM-DD').raw().allow(null).greater(Joi.ref('launched')),
replaced_by: Joi.string() replaced_by: Joi.string()
.regex(/^[A-Za-z0-9]+\.[a-z]{2}$/) .regex(/^[A-Za-z0-9]+\.[a-z]{2}($|@[A-Za-z0-9]+$)/)
.allow(null), .allow(null),
website: Joi.string() website: Joi.string()
.regex(/,/, { invert: true }) .regex(/,/, { invert: true })

41
scripts/schemes/feeds.ts Normal file
View file

@ -0,0 +1,41 @@
import BaseJoi from 'joi'
import JoiDate from '@joi/date'
const Joi = BaseJoi.extend(JoiDate)
export default {
channel: Joi.string()
.regex(/^[A-Za-z0-9]+\.[a-z]{2}$/)
.required(),
id: Joi.string()
.regex(/^[A-Za-z0-9]+$/)
.required(),
name: Joi.string()
.regex(/^[a-z0-9-!:&.+'/»#%°$@?|¡–\s_—]+$/i)
.regex(/^((?!\s-\s).)*$/)
.required(),
is_main: Joi.boolean().strict().required(),
broadcast_area: Joi.array().items(
Joi.string()
.regex(/^(s\/[A-Z]{2}-[A-Z0-9]{1,3}|c\/[A-Z]{2}|r\/[A-Z0-9]{2,7})$/)
.required()
),
timezones: Joi.array().items(
Joi.string()
.regex(/^[a-z-_/]+$/i)
.required()
),
languages: Joi.array().items(
Joi.string()
.regex(/^[a-z]{3}$/)
.required()
),
video_format: Joi.string()
.regex(/^\d+(i|p)$/)
.allow(null),
launched: Joi.date().format('YYYY-MM-DD').raw().allow(null),
closed: Joi.date().format('YYYY-MM-DD').raw().allow(null).greater(Joi.ref('launched')),
replaced_by: Joi.string()
.regex(/^[A-Za-z0-9]+\.[a-z]{2}($|@[A-Za-z0-9]+$)/)
.allow(null)
}

View file

@ -5,6 +5,8 @@ import { default as languages } from './languages'
import { default as regions } from './regions' import { default as regions } from './regions'
import { default as subdivisions } from './subdivisions' import { default as subdivisions } from './subdivisions'
import { default as blocklist } from './blocklist' import { default as blocklist } from './blocklist'
import { default as feeds } from './feeds'
import { default as timezones } from './timezones'
export default { export default {
channels, channels,
@ -13,5 +15,7 @@ export default {
languages, languages,
regions, regions,
subdivisions, subdivisions,
blocklist blocklist,
feeds,
timezones
} }

View file

@ -0,0 +1,11 @@
import Joi from 'joi'
export default {
id: Joi.string()
.regex(/^[a-z-_/]+$/i)
.required(),
utc_offset: Joi.string()
.regex(/^(\+|-)\d{2}:\d{2}$/)
.required(),
countries: Joi.array().items(Joi.string().regex(/^[A-Z]{2}$/))
}

24
scripts/utils.ts Normal file
View file

@ -0,0 +1,24 @@
export function createChannelId(
name: string | undefined,
country: string | undefined
): string | undefined {
if (!name || !country) return undefined
const slug = normalize(name)
const code = country.toLowerCase()
return `${slug}.${code}`
}
export function createFeedId(name: string) {
return normalize(name)
}
function normalize(string: string) {
return string
.replace(/^@/gi, 'At')
.replace(/^&/i, 'And')
.replace(/\+/gi, 'Plus')
.replace(/\s-(\d)/gi, ' Minus$1')
.replace(/[^a-z\d]+/gi, '')
}