Update scripts

This commit is contained in:
freearhey 2023-09-15 18:40:35 +03:00
parent 8a83f23243
commit f1d2add19a
98 changed files with 2423 additions and 1499 deletions

View file

@ -1,41 +0,0 @@
const _ = require('lodash')
const file = require('./file')
const DATA_DIR = process.env.DATA_DIR || './scripts/tmp/data'
class API {
constructor(filepath) {
this.filepath = file.resolve(filepath)
}
async load() {
const data = await file.read(this.filepath)
this.collection = JSON.parse(data)
}
find(query) {
return _.find(this.collection, query)
}
filter(query) {
return _.filter(this.collection, query)
}
all() {
return this.collection
}
}
const api = {}
api.channels = new API(`${DATA_DIR}/channels.json`)
api.streams = new API(`${DATA_DIR}/streams.json`)
api.countries = new API(`${DATA_DIR}/countries.json`)
api.guides = new API(`${DATA_DIR}/guides.json`)
api.categories = new API(`${DATA_DIR}/categories.json`)
api.languages = new API(`${DATA_DIR}/languages.json`)
api.regions = new API(`${DATA_DIR}/regions.json`)
api.blocklist = new API(`${DATA_DIR}/blocklist.json`)
api.subdivisions = new API(`${DATA_DIR}/subdivisions.json`)
module.exports = api

175
scripts/core/collection.ts Normal file
View file

@ -0,0 +1,175 @@
import _ from 'lodash'
import { orderBy, Order } from 'natural-orderby'
import { Dictionary } from './'
type Iteratee = (value: any, value2?: any) => void
export class Collection {
_items: any[]
constructor(items?: any[]) {
this._items = Array.isArray(items) ? items : []
}
first(predicate?: Iteratee) {
if (predicate) {
return this._items.find(predicate)
}
return this._items[0]
}
last(predicate?: Iteratee) {
if (predicate) {
return _.findLast(this._items, predicate)
}
return this._items[this._items.length - 1]
}
find(iteratee: Iteratee): Collection {
const found = this._items.filter(iteratee)
return new Collection(found)
}
add(data: any) {
this._items.push(data)
return this
}
intersects(collection: Collection): boolean {
return _.intersection(this._items, collection.all()).length > 0
}
count() {
return this._items.length
}
join(separator: string) {
return this._items.join(separator)
}
indexOf(value: string) {
return this._items.indexOf(value)
}
push(data: any) {
this.add(data)
}
uniq() {
const items = _.uniq(this._items)
return new Collection(items)
}
reduce(iteratee: Iteratee, accumulator: any) {
const items = _.reduce(this._items, iteratee, accumulator)
return new Collection(items)
}
filter(iteratee: Iteratee) {
const items = _.filter(this._items, iteratee)
return new Collection(items)
}
forEach(iteratee: Iteratee) {
for (let item of this._items) {
iteratee(item)
}
return this
}
remove(iteratee: Iteratee): Collection {
const removed = _.remove(this._items, iteratee)
return new Collection(removed)
}
concat(collection: Collection) {
const items = this._items.concat(collection._items)
return new Collection(items)
}
isEmpty(): boolean {
return this._items.length === 0
}
notEmpty(): boolean {
return this._items.length > 0
}
sort() {
const items = this._items.sort()
return new Collection(items)
}
orderBy(iteratees: Iteratee | Iteratee[], orders?: Order | Order[]) {
const items = orderBy(this._items, iteratees, orders)
return new Collection(items)
}
keyBy(iteratee: Iteratee) {
const items = _.keyBy(this._items, iteratee)
return new Dictionary(items)
}
empty() {
return this._items.length === 0
}
includes(value: any) {
if (typeof value === 'function') {
const found = this._items.find(value)
return !!found
}
return this._items.includes(value)
}
missing(value: any) {
if (typeof value === 'function') {
const found = this._items.find(value)
return !found
}
return !this._items.includes(value)
}
uniqBy(iteratee: Iteratee) {
const items = _.uniqBy(this._items, iteratee)
return new Collection(items)
}
groupBy(iteratee: Iteratee) {
const object = _.groupBy(this._items, iteratee)
return new Dictionary(object)
}
map(iteratee: Iteratee) {
const items = this._items.map(iteratee)
return new Collection(items)
}
all() {
return this._items
}
toJSON() {
return JSON.stringify(this._items)
}
}

22
scripts/core/database.ts Normal file
View file

@ -0,0 +1,22 @@
import Datastore from '@seald-io/nedb'
import * as path from 'path'
export class Database {
rootDir: string
constructor(rootDir: string) {
this.rootDir = rootDir
}
async load(filepath: string) {
const absFilepath = path.join(this.rootDir, filepath)
return new Datastore({
filename: path.resolve(absFilepath),
autoload: true,
onload: (error: Error): any => {
if (error) console.error(error.message)
}
})
}
}

View file

@ -1,12 +0,0 @@
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
const date = {}
date.utc = d => {
return dayjs.utc(d)
}
module.exports = date

View file

@ -1,82 +0,0 @@
const nedb = require('nedb-promises')
const fs = require('fs-extra')
const file = require('./file')
const DB_DIR = process.env.DB_DIR || './scripts/tmp/database'
fs.ensureDirSync(DB_DIR)
class Database {
constructor(filepath) {
this.filepath = filepath
}
load() {
this.db = nedb.create({
filename: file.resolve(this.filepath),
autoload: true,
onload: err => {
if (err) console.error(err)
},
compareStrings: (a, b) => {
a = a.replace(/\s/g, '_')
b = b.replace(/\s/g, '_')
return a.localeCompare(b, undefined, {
sensitivity: 'accent',
numeric: true
})
}
})
}
removeIndex(field) {
return this.db.removeIndex(field)
}
addIndex(options) {
return this.db.ensureIndex(options)
}
compact() {
return this.db.persistence.compactDatafile()
}
stopAutocompact() {
return this.db.persistence.stopAutocompaction()
}
reset() {
return file.clear(this.filepath)
}
count(query) {
return this.db.count(query)
}
insert(doc) {
return this.db.insert(doc)
}
update(query, update) {
return this.db.update(query, update)
}
find(query) {
return this.db.find(query)
}
all() {
return this.find({})
}
remove(query, options) {
return this.db.remove(query, options)
}
}
const db = {}
db.streams = new Database(`${DB_DIR}/streams.db`)
module.exports = db

View file

@ -0,0 +1,31 @@
export class Dictionary {
dict: any
constructor(dict?: any) {
this.dict = dict || {}
}
set(key: string, value: any) {
this.dict[key] = value
}
has(key: string): boolean {
return !!this.dict[key]
}
missing(key: string): boolean {
return !this.dict[key]
}
get(key: string): any {
return this.dict[key] ? this.dict[key] : undefined
}
keys(): string[] {
return Object.keys(this.dict)
}
data() {
return this.dict
}
}

View file

@ -1,70 +0,0 @@
const { create: createPlaylist } = require('./playlist')
const store = require('./store')
const path = require('path')
const glob = require('glob')
const fs = require('fs-extra')
const _ = require('lodash')
const file = {}
file.list = function (pattern) {
return new Promise(resolve => {
glob(pattern, function (err, files) {
resolve(files)
})
})
}
file.getFilename = function (filepath) {
return path.parse(filepath).name
}
file.createDir = async function (dir) {
if (await file.exists(dir)) return
return fs.mkdir(dir, { recursive: true }).catch(console.error)
}
file.exists = function (filepath) {
return fs.exists(path.resolve(filepath))
}
file.read = function (filepath) {
return fs.readFile(path.resolve(filepath), { encoding: 'utf8' }).catch(console.error)
}
file.append = function (filepath, data) {
return fs.appendFile(path.resolve(filepath), data).catch(console.error)
}
file.create = function (filepath, data = '') {
filepath = path.resolve(filepath)
const dir = path.dirname(filepath)
return file
.createDir(dir)
.then(() => fs.writeFile(filepath, data, { encoding: 'utf8', flag: 'w' }))
.catch(console.error)
}
file.write = function (filepath, data = '') {
return fs.writeFile(path.resolve(filepath), data).catch(console.error)
}
file.clear = function (filepath) {
return file.write(filepath, '')
}
file.resolve = function (filepath) {
return path.resolve(filepath)
}
file.dirname = function (filepath) {
return path.dirname(filepath)
}
file.basename = function (filepath) {
return path.basename(filepath)
}
module.exports = file

31
scripts/core/file.ts Normal file
View file

@ -0,0 +1,31 @@
import * as path from 'path'
export class File {
filepath: string
content: string
constructor(filepath: string, content?: string) {
this.filepath = filepath
this.content = content || ''
}
getFilename() {
return path.parse(this.filepath).name
}
dirname() {
return path.dirname(this.filepath)
}
basename() {
return path.basename(this.filepath)
}
append(data: string) {
this.content = this.content + data
}
extension() {
return this.filepath.split('.').pop()
}
}

View file

@ -1,33 +0,0 @@
const { create: createPlaylist } = require('./playlist')
const generators = require('../generators')
const logger = require('./logger')
const file = require('./file')
const PUBLIC_DIR = process.env.PUBLIC_DIR || '.gh-pages'
const LOGS_DIR = process.env.LOGS_DIR || 'scripts/tmp/logs/generators'
const generator = {}
generator.generate = async function (name, streams = []) {
if (typeof generators[name] === 'function') {
try {
let output = await generators[name].bind()(streams)
output = Array.isArray(output) ? output : [output]
for (const type of output) {
const playlist = createPlaylist(type.items, { public: true })
await file.create(`${PUBLIC_DIR}/${type.filepath}`, playlist.toString())
}
await file.create(`${LOGS_DIR}/${name}.log`, output.map(toJSON).join('\n'))
} catch (error) {
logger.error(`generators/${name}.js: ${error.message}`)
}
}
}
module.exports = generator
function toJSON(type) {
type.count = type.items.length
delete type.items
return JSON.stringify(type)
}

46
scripts/core/htmlTable.ts Normal file
View file

@ -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 = '<table>\n'
output += ' <thead>\n <tr>'
for (let column of this.columns) {
output += `<th align="left">${column.name}</th>`
}
output += '</tr>\n </thead>\n'
output += ' <tbody>\n'
for (let item of this.data) {
output += ' <tr>'
let i = 0
for (let prop in item) {
const column = this.columns[i]
let nowrap = column.nowrap ? ` nowrap` : ''
let align = column.align ? ` align="${column.align}"` : ''
output += `<td${align}${nowrap}>${item[prop]}</td>`
i++
}
output += '</tr>\n'
}
output += ' </tbody>\n'
output += '</table>'
return output
}
}

View file

@ -1,19 +0,0 @@
const { transliterate } = require('transliteration')
const id = {}
id.generate = function (name, code) {
if (!name || !code) return null
name = name.replace(/ *\([^)]*\) */g, '')
name = name.replace(/ *\[[^)]*\] */g, '')
name = name.replace(/\+/gi, 'Plus')
name = name.replace(/[^a-z\d]+/gi, '')
name = name.trim()
name = transliterate(name)
code = code.toLowerCase()
return `${name}.${code}`
}
module.exports = id

View file

@ -1,14 +0,0 @@
exports.db = require('./db')
exports.logger = require('./logger')
exports.file = require('./file')
exports.timer = require('./timer')
exports.parser = require('./parser')
exports.checker = require('./checker')
exports.generator = require('./generator')
exports.playlist = require('./playlist')
exports.store = require('./store')
exports.markdown = require('./markdown')
exports.api = require('./api')
exports.id = require('./id')
exports.m3u = require('./m3u')
exports.date = require('./date')

14
scripts/core/index.ts Normal file
View file

@ -0,0 +1,14 @@
export * from './database'
export * from './logger'
export * from './playlistParser'
export * from './numberParser'
export * from './logParser'
export * from './markdown'
export * from './file'
export * from './collection'
export * from './dictionary'
export * from './storage'
export * from './url'
export * from './issueLoader'
export * from './issueParser'
export * from './htmlTable'

View file

@ -0,0 +1,46 @@
import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods'
import { paginateRest } from '@octokit/plugin-paginate-rest'
import { Octokit } from '@octokit/core'
import { Collection, 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: any[] = []
if (TESTING) {
switch (labels) {
case 'streams:add':
issues = (await import('../../tests/__data__/input/issues/streams_add')).default
break
case 'streams:add,approved':
issues = (await import('../../tests/__data__/input/issues/streams_add_approved')).default
break
case 'streams:edit,approved':
issues = (await import('../../tests/__data__/input/issues/streams_edit_approved')).default
break
case 'streams:remove,approved':
issues = (await import('../../tests/__data__/input/issues/streams_remove_approved'))
.default
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)
}
}

View file

@ -0,0 +1,48 @@
import { Dictionary } from './'
export class IssueParser {
parse(issue: any): Dictionary {
const data = new Dictionary()
data.set('issue_number', issue.number)
const idDict = new Dictionary({
'Channel ID': 'channel_id',
'Channel ID (required)': 'channel_id',
'Broken Link': 'stream_url',
'Stream URL': 'stream_url',
'Stream URL (optional)': 'stream_url',
'Stream URL (required)': 'stream_url',
Label: 'label',
Quality: 'quality',
'Channel Name': 'channel_name',
'HTTP User-Agent': 'user_agent',
'HTTP Referrer': 'http_referrer',
Reason: 'reason',
'What happened to the stream?': 'reason',
'Possible Replacement (optional)': 'possible_replacement',
Notes: 'notes',
'Notes (optional)': 'notes'
})
const fields = issue.body.split('###')
if (!fields.length) return data
fields.forEach((field: string) => {
let [_label, , _value] = field.split(/\r?\n/)
_label = _label ? _label.trim() : ''
_value = _value ? _value.trim() : ''
if (!_label || !_value) return data
const id: string = idDict.get(_label)
const value: string = _value === '_No response_' || _value === 'None' ? '' : _value
if (!id) return
data.set(id, value)
})
return data
}
}

13
scripts/core/logParser.ts Normal file
View file

@ -0,0 +1,13 @@
export type LogItem = {
filepath: string
count: number
}
export class LogParser {
parse(content: string): any[] {
if (!content) return []
const lines = content.split('\n')
return lines.map(line => (line ? JSON.parse(line) : null)).filter(l => l)
}
}

View file

@ -1,13 +0,0 @@
const { Signale } = require('signale')
const options = {}
const logger = new Signale(options)
logger.config({
displayLabel: false,
displayScope: false,
displayBadge: false
})
module.exports = logger

9
scripts/core/logger.ts Normal file
View file

@ -0,0 +1,9 @@
import signale from 'signale'
const { Signale } = signale
export class Logger extends Signale {
constructor(options?: any) {
super(options)
}
}

View file

@ -1,34 +0,0 @@
const m3u = {}
m3u.create = function (links = [], header = {}) {
let output = `#EXTM3U`
for (const attr in header) {
const value = header[attr]
output += ` ${attr}="${value}"`
}
output += `\n`
for (const link of links) {
output += `#EXTINF:-1`
for (const name in link.attrs) {
const value = link.attrs[name]
if (value !== undefined) {
output += ` ${name}="${value}"`
}
}
output += `,${link.title}\n`
for (const name in link.vlcOpts) {
const value = link.vlcOpts[name]
if (value !== undefined) {
output += `#EXTVLCOPT:${name}=${value}\n`
}
}
output += `${link.url}\n`
}
return output
}
module.exports = m3u

View file

@ -1,10 +0,0 @@
const markdownInclude = require('markdown-include')
const file = require('./file')
const markdown = {}
markdown.compile = function (filepath) {
markdownInclude.compileFiles(file.resolve(filepath))
}
module.exports = markdown

13
scripts/core/markdown.ts Normal file
View file

@ -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)
}
}

View file

@ -0,0 +1,10 @@
export default class NumberParser {
async parse(number: string) {
const parsed = parseInt(number)
if (isNaN(parsed)) {
throw new Error('numberParser:parse() Input value is not a number')
}
return parsed
}
}

View file

@ -1,30 +0,0 @@
const ipp = require('iptv-playlist-parser')
const logger = require('./logger')
const file = require('./file')
const parser = {}
parser.parsePlaylist = async function (filepath) {
const content = await file.read(filepath)
return ipp.parse(content)
}
parser.parseLogs = async function (filepath) {
const content = await file.read(filepath)
if (!content) return []
const lines = content.split('\n')
return lines.map(line => (line ? JSON.parse(line) : null)).filter(l => l)
}
parser.parseNumber = function (string) {
const parsed = parseInt(string)
if (isNaN(parsed)) {
throw new Error('scripts/core/parser.js:parseNumber() Input value is not a number')
}
return parsed
}
module.exports = parser

View file

@ -1,53 +0,0 @@
const store = require('./store')
const m3u = require('./m3u')
const _ = require('lodash')
const playlist = {}
class Playlist {
constructor(items = [], options = {}) {
this.header = {}
this.links = []
for (const item of items) {
const stream = store.create(item)
let attrs
if (options.public) {
attrs = {
'tvg-id': stream.get('tvg_id'),
'tvg-logo': stream.get('tvg_logo'),
'group-title': stream.get('group_title'),
'user-agent': stream.get('user_agent') || undefined
}
} else {
attrs = {
'tvg-id': stream.get('tvg_id'),
'user-agent': stream.get('user_agent') || undefined
}
}
const vlcOpts = {
'http-referrer': stream.get('http_referrer') || undefined,
'http-user-agent': stream.get('user_agent') || undefined
}
this.links.push({
url: stream.get('url'),
title: stream.get('title'),
attrs,
vlcOpts
})
}
}
toString() {
return m3u.create(this.links, this.header)
}
}
playlist.create = function (items, options) {
return new Playlist(items, options)
}
module.exports = playlist

View file

@ -0,0 +1,45 @@
import parser from 'iptv-playlist-parser'
import { Playlist, Stream } from '../models'
import { Collection, Storage } from './'
export class PlaylistParser {
storage: Storage
constructor({ storage }: { storage: Storage }) {
this.storage = storage
}
async parse(filepath: string): Promise<Playlist> {
const streams = new Collection()
const content = await this.storage.read(filepath)
const parsed: parser.Playlist = parser.parse(content)
parsed.items.forEach((item: parser.PlaylistItem) => {
const { name, label, quality } = parseTitle(item.name)
const stream = new Stream({
channel: item.tvg.id,
name,
label,
quality,
filepath,
line: item.line,
url: item.url,
httpReferrer: item.http.referrer,
userAgent: item.http['user-agent']
})
streams.add(stream)
})
return new Playlist(streams)
}
}
function parseTitle(title: string): { name: string; label: string; quality: string } {
const [, label] = title.match(/ \[(.*)\]$/) || [null, '']
const [, quality] = title.match(/ \(([0-9]+p)\)/) || [null, '']
const name = title.replace(` (${quality})`, '').replace(` [${label}]`, '')
return { name, label, quality }
}

82
scripts/core/storage.ts Normal file
View file

@ -0,0 +1,82 @@
import { File, Collection } from './'
import * as path from 'path'
import fs from 'fs-extra'
import { glob } from 'glob'
export class Storage {
rootDir: string
constructor(rootDir?: string) {
this.rootDir = rootDir || './'
}
list(pattern: string): Promise<string[]> {
return glob(pattern, {
cwd: this.rootDir
})
}
async createDir(dir: string): Promise<void> {
if (await fs.exists(dir)) return
await fs.mkdir(dir, { recursive: true }).catch(console.error)
}
async load(filepath: string): Promise<any> {
return this.read(filepath)
}
async read(filepath: string): Promise<any> {
const absFilepath = path.join(this.rootDir, filepath)
return await fs.readFile(absFilepath, { encoding: 'utf8' })
}
async json(filepath: string): Promise<any> {
const absFilepath = path.join(this.rootDir, filepath)
const content = await fs.readFile(absFilepath, { encoding: 'utf8' })
return JSON.parse(content)
}
async exists(filepath: string): Promise<boolean> {
const absFilepath = path.join(this.rootDir, filepath)
return await fs.exists(absFilepath)
}
async write(filepath: string, data: string = ''): Promise<void> {
const absFilepath = path.join(this.rootDir, filepath)
const dir = path.dirname(absFilepath)
await this.createDir(dir)
await fs.writeFile(absFilepath, data, { encoding: 'utf8', flag: 'w' })
}
async append(filepath: string, data: string = ''): Promise<void> {
const absFilepath = path.join(this.rootDir, filepath)
await fs.appendFile(absFilepath, data, { encoding: 'utf8', flag: 'w' })
}
async clear(filepath: string): Promise<void> {
await this.write(filepath)
}
async createStream(filepath: string): Promise<NodeJS.WriteStream> {
const absFilepath = path.join(this.rootDir, filepath)
const dir = path.dirname(absFilepath)
await this.createDir(dir)
return fs.createWriteStream(absFilepath) as unknown as NodeJS.WriteStream
}
async save(filepath: string, content: string): Promise<void> {
await this.write(filepath, content)
}
async saveFile(file: File): Promise<void> {
await this.write(file.filepath, file.content)
}
}

View file

@ -1,56 +0,0 @@
const _ = require('lodash')
const logger = require('./logger')
const setters = require('../store/setters')
const getters = require('../store/getters')
module.exports = {
create(state = {}) {
return {
state,
changed: false,
set: function (prop, value) {
const prevState = JSON.stringify(this.state)
const setter = setters[prop]
if (typeof setter === 'function') {
try {
this.state[prop] = setter.bind()(value)
} catch (error) {
logger.error(`store/setters/${prop}.js: ${error.message}`)
}
} else if (typeof value === 'object') {
this.state[prop] = value[prop]
} else {
this.state[prop] = value
}
const newState = JSON.stringify(this.state)
if (prevState !== newState) {
this.changed = true
}
return this
},
get: function (prop) {
const getter = getters[prop]
if (typeof getter === 'function') {
try {
return getter.bind(this.state)()
} catch (error) {
logger.error(`store/getters/${prop}.js: ${error.message}`)
}
} else {
return prop.split('.').reduce((o, i) => (o ? o[i] : undefined), this.state)
}
},
has: function (prop) {
const value = this.get(prop)
return !_.isEmpty(value)
},
data: function () {
return this.state
}
}
}
}

View file

@ -1,32 +0,0 @@
const table = {}
table.create = function (data, cols) {
let output = '<table>\n'
output += ' <thead>\n <tr>'
for (let column of cols) {
output += `<th align="left">${column.name}</th>`
}
output += '</tr>\n </thead>\n'
output += ' <tbody>\n'
for (let item of data) {
output += ' <tr>'
let i = 0
for (let prop in item) {
const column = cols[i]
let nowrap = column.nowrap ? ` nowrap` : ''
let align = column.align ? ` align="${column.align}"` : ''
output += `<td${align}${nowrap}>${item[prop]}</td>`
i++
}
output += '</tr>\n'
}
output += ' </tbody>\n'
output += '</table>'
return output
}
module.exports = table

View file

@ -1,29 +0,0 @@
const { performance } = require('perf_hooks')
const dayjs = require('dayjs')
const duration = require('dayjs/plugin/duration')
const relativeTime = require('dayjs/plugin/relativeTime')
dayjs.extend(relativeTime)
dayjs.extend(duration)
const timer = {}
let t0 = 0
timer.start = function () {
t0 = performance.now()
}
timer.format = function (f) {
let t1 = performance.now()
return dayjs.duration(t1 - t0).format(f)
}
timer.humanize = function (suffix = true) {
let t1 = performance.now()
return dayjs.duration(t1 - t0).humanize(suffix)
}
module.exports = timer

View file

@ -1,11 +0,0 @@
const normalize = require('normalize-url')
const url = {}
url.normalize = function (string) {
const normalized = normalize(string, { stripWWW: false })
return decodeURIComponent(normalized).replace(/\s/g, '+')
}
module.exports = url

20
scripts/core/url.ts Normal file
View file

@ -0,0 +1,20 @@
import normalizeUrl from 'normalize-url'
export class URL {
url: string
constructor(url: string) {
this.url = url
}
normalize(): URL {
const normalized = normalizeUrl(this.url, { stripWWW: false })
this.url = decodeURIComponent(normalized).replace(/\s/g, '+')
return this
}
toString(): string {
return this.url
}
}