diff --git a/scripts/commands/sites/update.ts b/scripts/commands/sites/update.ts new file mode 100644 index 00000000..d1f7e8a4 --- /dev/null +++ b/scripts/commands/sites/update.ts @@ -0,0 +1,58 @@ +import { Logger, Storage, Collection, Dictionary } from '@freearhey/core' +import { IssueLoader, HTMLTable, Markdown } from '../../core' +import { Issue, Site } from '../../models' +import { SITES_DIR, DOT_SITES_DIR } from '../../constants' +import path from 'path' + +async function main() { + const logger = new Logger({ disabled: true }) + const loader = new IssueLoader() + const storage = new Storage(SITES_DIR) + const sites = new Collection() + + logger.info('loading list of sites') + const folders = await storage.list('*/') + + logger.info('loading issues...') + const issues = await loadIssues(loader) + + logger.info('putting the data together...') + folders.forEach((domain: string) => { + const filteredIssues = issues.filter((issue: Issue) => domain === issue.data.get('site')) + const site = new Site({ + domain, + issues: filteredIssues + }) + + sites.add(site) + }) + + logger.info('creating sites table...') + let data = new Collection() + sites.forEach((site: Site) => { + data.add([ + `${site.domain}`, + site.getStatus().emoji, + site.getIssues().all().join(', ') + ]) + }) + + const table = new HTMLTable(data.all(), [{ name: 'Site' }, { name: 'Status' }, { name: 'Notes' }]) + + const readmeStorage = new Storage(DOT_SITES_DIR) + await readmeStorage.save('_table.md', table.toString()) + + logger.info('updating sites.md...') + const configPath = path.join(DOT_SITES_DIR, 'config.json') + const sitesMarkdown = new Markdown(configPath) + sitesMarkdown.compile() +} + +main() + +async function loadIssues(loader: IssueLoader) { + const issuesWithStatusWarning = await loader.load({ labels: ['broken guide', 'status:warning'] }) + const issuesWithStatusDown = await loader.load({ labels: ['broken guide', 'status:down'] }) + + return issuesWithStatusWarning.concat(issuesWithStatusDown) +} diff --git a/scripts/constants.ts b/scripts/constants.ts index 7498bf81..e15b8b45 100644 --- a/scripts/constants.ts +++ b/scripts/constants.ts @@ -2,3 +2,7 @@ export const SITES_DIR = process.env.SITES_DIR || './sites' export const GUIDES_DIR = process.env.GUIDES_DIR || './guides' export const DATA_DIR = process.env.DATA_DIR || './temp/data' export const API_DIR = process.env.API_DIR || '.api' +export const DOT_SITES_DIR = process.env.DOT_SITES_DIR || './.sites' +export const TESTING = process.env.NODE_ENV === 'test' ? true : false +export const OWNER = 'iptv-org' +export const REPO = 'epg' diff --git a/scripts/core/htmlTable.ts b/scripts/core/htmlTable.ts new file mode 100644 index 00000000..1caa85fa --- /dev/null +++ b/scripts/core/htmlTable.ts @@ -0,0 +1,46 @@ +type Column = { + name: string + nowrap?: boolean + align?: string +} + +type DataItem = string[] + +export class HTMLTable { + data: DataItem[] + columns: Column[] + + constructor(data: DataItem[], columns: Column[]) { + this.data = data + this.columns = columns + } + + toString() { + let output = '\n' + + output += ' \n ' + for (const column of this.columns) { + output += `` + } + output += '\n \n' + + output += ' \n' + for (const item of this.data) { + output += ' ' + let i = 0 + for (const prop in item) { + const column = this.columns[i] + const nowrap = column.nowrap ? ' nowrap' : '' + const align = column.align ? ` align="${column.align}"` : '' + output += `${item[prop]}` + i++ + } + output += '\n' + } + output += ' \n' + + output += '
${column.name}
' + + return output + } +} diff --git a/scripts/core/index.ts b/scripts/core/index.ts index fca249a5..14a0ffa0 100644 --- a/scripts/core/index.ts +++ b/scripts/core/index.ts @@ -10,3 +10,7 @@ export * from './guide' export * from './apiChannel' export * from './apiClient' export * from './queueCreator' +export * from './issueLoader' +export * from './issueParser' +export * from './htmlTable' +export * from './markdown' diff --git a/scripts/core/issueLoader.ts b/scripts/core/issueLoader.ts new file mode 100644 index 00000000..28697e58 --- /dev/null +++ b/scripts/core/issueLoader.ts @@ -0,0 +1,40 @@ +import { Collection } from '@freearhey/core' +import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods' +import { paginateRest } from '@octokit/plugin-paginate-rest' +import { Octokit } from '@octokit/core' +import { IssueParser } from './' +import { TESTING, OWNER, REPO } from '../constants' + +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 + let issues: object[] = [] + if (TESTING) { + switch (labels) { + case 'broken guide,status:warning': + issues = require('../../tests/__data__/input/issues/broken_guide_warning.js') + break + case 'broken guide,status:down': + issues = require('../../tests/__data__/input/issues/broken_guide_down.js') + break + } + } else { + issues = await octokit.paginate(octokit.rest.issues.listForRepo, { + owner: OWNER, + repo: REPO, + per_page: 100, + labels, + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }) + } + + const parser = new IssueParser() + + return new Collection(issues).map(parser.parse) + } +} diff --git a/scripts/core/issueParser.ts b/scripts/core/issueParser.ts new file mode 100644 index 00000000..2fe2ddd8 --- /dev/null +++ b/scripts/core/issueParser.ts @@ -0,0 +1,34 @@ +import { Dictionary } from '@freearhey/core' +import { Issue } from '../models' + +const FIELDS = new Dictionary({ + Site: 'site' +}) + +export class IssueParser { + parse(issue: { number: number; body: string; labels: { name: string }[] }): Issue { + const fields = issue.body.split('###') + + const data = new Dictionary() + fields.forEach((field: string) => { + let parsed = field.split(/\r?\n/).filter(Boolean) + let _label = parsed.shift() + _label = _label ? _label.trim() : '' + let _value = parsed.join('\r\n') + _value = _value ? _value.trim() : '' + + if (!_label || !_value) return data + + const id: string = FIELDS.get(_label) + const value: string = _value === '_No response_' || _value === 'None' ? '' : _value + + if (!id) return + + data.set(id, value) + }) + + const labels = issue.labels.map(label => label.name) + + return new Issue({ number: issue.number, labels, data }) + } +} diff --git a/scripts/core/markdown.ts b/scripts/core/markdown.ts new file mode 100644 index 00000000..b43b5608 --- /dev/null +++ b/scripts/core/markdown.ts @@ -0,0 +1,13 @@ +import markdownInclude from 'markdown-include' + +export class Markdown { + filepath: string + + constructor(filepath: string) { + this.filepath = filepath + } + + compile() { + markdownInclude.compileFiles(this.filepath) + } +} diff --git a/scripts/models/index.ts b/scripts/models/index.ts new file mode 100644 index 00000000..1065611d --- /dev/null +++ b/scripts/models/index.ts @@ -0,0 +1,2 @@ +export * from './issue' +export * from './site' diff --git a/scripts/models/issue.ts b/scripts/models/issue.ts new file mode 100644 index 00000000..7eb241e5 --- /dev/null +++ b/scripts/models/issue.ts @@ -0,0 +1,24 @@ +import { Dictionary } from '@freearhey/core' +import { OWNER, REPO } from '../constants' + +type IssueProps = { + number: number + labels: string[] + data: Dictionary +} + +export class Issue { + number: number + labels: string[] + data: Dictionary + + constructor({ number, labels, data }: IssueProps) { + this.number = number + this.labels = labels + this.data = data + } + + getURL() { + return `https://github.com/${OWNER}/${REPO}/issues/${this.number}` + } +} diff --git a/scripts/models/site.ts b/scripts/models/site.ts new file mode 100644 index 00000000..1e7dcadd --- /dev/null +++ b/scripts/models/site.ts @@ -0,0 +1,57 @@ +import { Collection } from '@freearhey/core' +import { Issue } from './' + +enum StatusCode { + DOWN = 'down', + WARNING = 'warning', + OK = 'ok' +} + +type Status = { + code: StatusCode + emoji: string +} + +type SiteProps = { + domain: string + issues: Collection +} + +export class Site { + domain: string + issues: Collection + + constructor({ domain, issues }: SiteProps) { + this.domain = domain + this.issues = issues + } + + getStatus(): Status { + const issuesWithStatusDown = this.issues.filter((issue: Issue) => + issue.labels.find(label => label === 'status:down') + ) + if (issuesWithStatusDown.notEmpty()) + return { + code: StatusCode.DOWN, + emoji: '🔴' + } + + const issuesWithStatusWarning = this.issues.filter((issue: Issue) => + issue.labels.find(label => label === 'status:warning') + ) + if (issuesWithStatusWarning.notEmpty()) + return { + code: StatusCode.WARNING, + emoji: '🟡' + } + + return { + code: StatusCode.OK, + emoji: '🟢' + } + } + + getIssues(): Collection { + return this.issues.map((issue: Issue) => issue.getURL()) + } +}