diff --git a/scripts/core/csvParser.ts b/scripts/core/csvParser.ts index 15451a06..27aa70cc 100644 --- a/scripts/core/csvParser.ts +++ b/scripts/core/csvParser.ts @@ -21,7 +21,10 @@ const opts = { replaced_by: nullable, website: nullable, logo: nullable, - countries: listParser + countries: listParser, + timezones: listParser, + is_main: boolParser, + video_format: nullable } } diff --git a/scripts/core/idCreator.ts b/scripts/core/idCreator.ts deleted file mode 100644 index e547f83e..00000000 --- a/scripts/core/idCreator.ts +++ /dev/null @@ -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, '') -} diff --git a/scripts/core/index.ts b/scripts/core/index.ts index 387d00ea..11090373 100644 --- a/scripts/core/index.ts +++ b/scripts/core/index.ts @@ -2,6 +2,5 @@ export * from './csv' export * from './issueParser' export * from './issueLoader' export * from './csvParser' -export * from './idCreator' export * from './issueData' export * from './issue' diff --git a/scripts/core/issueLoader.ts b/scripts/core/issueLoader.ts index 8d588f12..a514fea0 100644 --- a/scripts/core/issueLoader.ts +++ b/scripts/core/issueLoader.ts @@ -9,32 +9,15 @@ const CustomOctokit = Octokit.plugin(paginateRest, restEndpointMethods) const octokit = new CustomOctokit() export class IssueLoader { - async load({ labels }: { labels: string[] | string }) { - labels = Array.isArray(labels) ? labels.join(',') : labels + async load(props?: { labels: string[] | string }) { + let labels = '' + if (props && props.labels) { + labels = Array.isArray(props.labels) ? props.labels.join(',') : props.labels + } + let issues: object[] = [] if (TESTING) { - switch (labels) { - 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 - } + issues = (await import('../../tests/__data__/input/update/issues.js')).default } else { issues = await octokit.paginate(octokit.rest.issues.listForRepo, { owner: OWNER, diff --git a/scripts/core/issueParser.ts b/scripts/core/issueParser.ts index ac74649c..b051b5a1 100644 --- a/scripts/core/issueParser.ts +++ b/scripts/core/issueParser.ts @@ -3,40 +3,30 @@ import { IssueData, Issue } from '../core' const FIELDS = new Dictionary({ 'Channel ID': 'channel_id', - 'Channel ID (required)': 'channel_id', - 'Channel ID (optional)': 'channel_id', - 'Channel Name': 'name', + 'Channel Name': 'channel_name', + 'Feed Name': 'feed_name', + 'Feed ID': 'feed_id', + 'Main Feed': 'is_main', 'Alternative Names': 'alt_names', - 'Alternative Names (optional)': 'alt_names', Network: 'network', - 'Network (optional)': 'network', Owners: 'owners', - 'Owners (optional)': 'owners', Country: 'country', Subdivision: 'subdivision', - 'Subdivision (optional)': 'subdivision', City: 'city', - 'City (optional)': 'city', 'Broadcast Area': 'broadcast_area', + Timezones: 'timezones', + Format: 'video_format', Languages: 'languages', Categories: 'categories', - 'Categories (optional)': 'categories', NSFW: 'is_nsfw', Launched: 'launched', - 'Launched (optional)': 'launched', Closed: 'closed', - 'Closed (optional)': 'closed', 'Replaced By': 'replaced_by', - 'Replaced By (optional)': 'replaced_by', Website: 'website', - 'Website (optional)': 'website', Logo: 'logo', Reason: 'reason', Notes: 'notes', - 'Notes (optional)': 'notes', - Reference: 'ref', - 'Reference (optional)': 'ref', - 'Reference (required)': 'ref' + Reference: 'ref' }) export class IssueParser { @@ -46,7 +36,7 @@ export class IssueParser { const data = new Dictionary() fields.forEach((field: string) => { let [_label, , _value] = field.split(/\r?\n/) - _label = _label ? _label.trim() : '' + _label = _label ? _label.replace(/ \(optional\)| \(required\)/, '').trim() : '' _value = _value ? _value.trim() : '' if (!_label || !_value) return data diff --git a/scripts/db/update.ts b/scripts/db/update.ts index d8234736..4f632348 100644 --- a/scripts/db/update.ts +++ b/scripts/db/update.ts @@ -1,36 +1,49 @@ -import { CSV, IssueLoader, CSVParser, IDCreator, Issue, IssueData } from '../core' -import { Channel, Blocked } from '../models' +import { CSV, IssueLoader, CSVParser, Issue, IssueData } from '../core' +import { Channel, Blocked, Feed } from '../models' import { DATA_DIR } from '../constants' import { Storage, Collection } from '@freearhey/core' +import { createChannelId, createFeedId } from '../utils' let blocklist = new Collection() let channels = new Collection() +let feeds = new Collection() +let issues = new Collection() const processedIssues = new Collection() async function main() { - const idCreator = new IDCreator() const dataStorage = new Storage(DATA_DIR) 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() - await removeChannels({ loader }) - await editChannels({ loader, idCreator }) - await addChannels({ loader, idCreator }) - await blockChannels({ loader }) - await unblockChannels({ loader }) + issues = await loader.load() - 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() 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() await dataStorage.save('blocklist.csv', blocklistOutput) @@ -40,21 +53,139 @@ async function main() { main() -function sortBy(channels: Collection, key: string) { - const items = channels.all().sort((a, b) => { - const normA = a[key].toLowerCase() - const normB = b[key].toLowerCase() - if (normA < normB) return -1 - if (normA > normB) return 1 - return 0 - }) +async function removeFeeds() { + const requests = issues.filter( + issue => issue.labels.includes('feeds:remove') && issue.labels.includes('approved') + ) - 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 }) { - const issues = await loader.load({ labels: ['channels:remove,approved'] }) - issues.forEach((issue: Issue) => { +async function editFeeds() { + const requests = issues.filter( + 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 const found = channels.first( @@ -64,13 +195,18 @@ async function removeChannels({ loader }: { loader: IssueLoader }) { channels.remove((channel: Channel) => channel.id === found.id) + onChannelRemoval(found.id) + processedIssues.push(issue) }) } -async function editChannels({ loader, idCreator }: { loader: IssueLoader; idCreator: IDCreator }) { - const issues = await loader.load({ labels: ['channels:edit,approved'] }) - issues.forEach((issue: Issue) => { +async function editChannels() { + const requests = issues.filter( + issue => issue.labels.includes('channels:edit') && issue.labels.includes('approved') + ) + + requests.forEach((issue: Issue) => { const data: IssueData = issue.data if (data.missing('channel_id')) return @@ -79,20 +215,21 @@ async function editChannels({ loader, idCreator }: { loader: IssueLoader; idCrea ) if (!found) return - let channelId = found.id - if (data.has('name') || data.has('country')) { - const name = data.getString('name') || found.name + let channelId: string | undefined = found.id + if (data.has('channel_name') || data.has('country')) { + const name = data.getString('channel_name') || found.name const country = data.getString('country') || found.country if (name && country) { - channelId = idCreator.create(name, country) - updateBlocklistId(found.id, channelId) - updateChannelReplacedBy(found.id, channelId) + channelId = createChannelId(name, country) + if (channelId) onChannelIdChange(found.id, channelId) } } + if (!channelId) return + const updated = new Channel({ id: channelId, - name: data.getString('name'), + name: data.getString('channel_name'), alt_names: data.getArray('alt_names'), network: data.getString('network'), 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 }) { - const issues = await loader.load({ labels: ['channels:add,approved'] }) - issues.forEach((issue: Issue) => { +async function addChannels() { + const requests = issues.filter( + issue => issue.labels.includes('channels:add') && issue.labels.includes('approved') + ) + + requests.forEach((issue: Issue) => { 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) if (found) return @@ -133,7 +283,7 @@ async function addChannels({ loader, idCreator }: { loader: IssueLoader; idCreat channels.push( new Channel({ id: channelId, - name: data.getString('name'), + name: data.getString('channel_name'), alt_names: data.getArray('alt_names'), network: data.getString('network'), 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) }) } -async function unblockChannels({ loader }: { loader: IssueLoader }) { - const issues = await loader.load({ labels: ['blocklist:remove,approved'] }) - issues.forEach((issue: Issue) => { +async function unblockChannels() { + const requests = issues.filter( + issue => issue.labels.includes('blocklist:remove') && issue.labels.includes('approved') + ) + + requests.forEach((issue: Issue) => { const data = issue.data if (data.missing('channel_id')) return @@ -173,9 +344,12 @@ async function unblockChannels({ loader }: { loader: IssueLoader }) { }) } -async function blockChannels({ loader }: { loader: IssueLoader }) { - const issues = await loader.load({ labels: ['blocklist:add,approved'] }) - issues.forEach((issue: Issue) => { +async function blockChannels() { + const requests = issues.filter( + issue => issue.labels.includes('blocklist:add') && issue.labels.includes('approved') + ) + + requests.forEach((issue: Issue) => { const data = issue.data if (data.missing('channel_id')) return @@ -201,22 +375,77 @@ async function blockChannels({ loader }: { loader: IssueLoader }) { }) } -function updateBlocklistId(oldId: string, newId: string) { - const filtered: Collection = blocklist.filter((blocked: Blocked) => blocked.channel === oldId) - if (filtered.isEmpty()) return +function onFeedIdChange(channelId: string, feedId: string, newFeedId: string) { + channels.forEach((channel: Channel) => { + if (channel.replaced_by && channel.replaced_by === `${channelId}@${feedId}`) { + channel.replaced_by = `${channelId}@${newFeedId}` + } + }) - filtered.forEach(item => { - item.channel = newId + feeds.forEach((feed: Feed) => { + if (feed.replaced_by && feed.replaced_by === `${channelId}@${feedId}`) { + feed.replaced_by = `${channelId}@${newFeedId}` + } }) } -function updateChannelReplacedBy(channelId: string, newReplacedBy: string) { - const filtered: Collection = channels.filter( - (channel: Channel) => channel.replaced_by === channelId - ) - if (filtered.isEmpty()) return - - filtered.forEach(item => { - item.replaced_by = newReplacedBy +function onFeedNewMain(channelId: string, feedId: string) { + feeds.forEach((feed: Feed) => { + if (feed.channel === channelId && feed.id !== feedId && feed.is_main === true) { + feed.is_main = false + } + }) +} + +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 = '' + } }) } diff --git a/scripts/db/validate.ts b/scripts/db/validate.ts index afb61ab8..eccae4ff 100644 --- a/scripts/db/validate.ts +++ b/scripts/db/validate.ts @@ -3,8 +3,9 @@ import { DATA_DIR } from '../constants' import schemesData from '../schemes' import { program } from 'commander' import Joi from 'joi' -import { CSVParser, IDCreator } from '../core' +import { CSVParser } from '../core' import chalk from 'chalk' +import { createChannelId } from '../utils' program.argument('[filepath]', 'Path to file to validate').parse(process.argv) @@ -42,11 +43,15 @@ async function main() { let grouped switch (filename) { + case 'feeds': + grouped = data.keyBy(item => item.channel + item.id) + break case 'blocklist': grouped = data.keyBy(item => item.channel + item.ref) break case 'categories': case 'channels': + case 'timezones': grouped = data.keyBy(item => item.id) break default: @@ -91,6 +96,17 @@ async function main() { ) } 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': fileErrors = fileErrors.concat(findDuplicatesBy(rowsCopy, ['channel', 'ref'])) for (const [i, row] of rowsCopy.entries()) { @@ -201,7 +217,7 @@ function findDuplicatesBy(rows: { [key: string]: string }[], keys: string[]) { const buffer = new Dictionary() 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)) { const fieldsList = keys.map(key => `${key} "${row[key]}"`).join(' and ') errors.push({ @@ -216,10 +232,31 @@ function findDuplicatesBy(rows: { [key: string]: string }[], keys: string[]) { 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) { const errors = new Collection() - const expectedId = new IDCreator().create(row.name, row.country) + const expectedId = createChannelId(row.name, row.country) if (expectedId !== row.id) { errors.push({ @@ -254,6 +291,22 @@ function validateChannelBroadcastArea(row: { [key: string]: string[] }, i: numbe 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) { logger.error(chalk.red(message)) process.exit(1) diff --git a/scripts/models/feed.ts b/scripts/models/feed.ts new file mode 100644 index 00000000..1f524c8a --- /dev/null +++ b/scripts/models/feed.ts @@ -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] + } + } +} diff --git a/scripts/models/index.ts b/scripts/models/index.ts index 2e6c7027..614b5a24 100644 --- a/scripts/models/index.ts +++ b/scripts/models/index.ts @@ -1,2 +1,3 @@ export * from './channel' export * from './blocked' +export * from './feed' diff --git a/scripts/schemes/channels.ts b/scripts/schemes/channels.ts index 25481753..026798b8 100644 --- a/scripts/schemes/channels.ts +++ b/scripts/schemes/channels.ts @@ -46,7 +46,7 @@ export default { 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}$/) + .regex(/^[A-Za-z0-9]+\.[a-z]{2}($|@[A-Za-z0-9]+$)/) .allow(null), website: Joi.string() .regex(/,/, { invert: true }) diff --git a/scripts/schemes/feeds.ts b/scripts/schemes/feeds.ts new file mode 100644 index 00000000..c1cc340f --- /dev/null +++ b/scripts/schemes/feeds.ts @@ -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) +} diff --git a/scripts/schemes/index.ts b/scripts/schemes/index.ts index b01922fb..10a96e92 100644 --- a/scripts/schemes/index.ts +++ b/scripts/schemes/index.ts @@ -5,6 +5,8 @@ import { default as languages } from './languages' import { default as regions } from './regions' import { default as subdivisions } from './subdivisions' import { default as blocklist } from './blocklist' +import { default as feeds } from './feeds' +import { default as timezones } from './timezones' export default { channels, @@ -13,5 +15,7 @@ export default { languages, regions, subdivisions, - blocklist + blocklist, + feeds, + timezones } diff --git a/scripts/schemes/timezones.ts b/scripts/schemes/timezones.ts new file mode 100644 index 00000000..d8da6043 --- /dev/null +++ b/scripts/schemes/timezones.ts @@ -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}$/)) +} diff --git a/scripts/utils.ts b/scripts/utils.ts new file mode 100644 index 00000000..84c76c90 --- /dev/null +++ b/scripts/utils.ts @@ -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, '') +}