22 |
29 |
36 |
43 |
44 |
45 |
46 |
47 |
49 |
50 |
57 |
58 |
59 |
60 |
62 |
69 |
76 |
77 |
78 |
79 |
87 | You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!
46 | Open Hosting Documentation 47 |Firebase SDK Loading…
49 | 50 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/Cue/Parser.test.ts: -------------------------------------------------------------------------------- 1 | import { timeEntryToString } from './Formatter'; 2 | import Parser, { ParserHelper } from './Parser'; 3 | 4 | const parserHelper = new ParserHelper(); 5 | const parser = new Parser(parserHelper); 6 | 7 | describe('Parser', () => { 8 | test('parseTitle', () => { 9 | const value = ' Russia Goes Clubbing 249 (2013-07-17) (Live @ Zouk, Singapore) '; 10 | const actual = parser.parseTitle(value); 11 | const expected = 'Russia Goes Clubbing 249 (2013-07-17) (Live @ Zouk, Singapore)'; 12 | expect(actual).toBe(expected); 13 | }); 14 | test('parsePerformer', () => { 15 | const value = ' Bobina '; 16 | const actual = parser.parsePerformer(value); 17 | const expected = 'Bobina'; 18 | expect(actual).toBe(expected); 19 | }); 20 | test('parseFileName', () => { 21 | const value = ' Bobina - Russia Goes Clubbing #249 [Live @ Zouk, Singapore].mp3 '; 22 | const actual = parser.parseFileName(value); 23 | const expected = 'Bobina - Russia Goes Clubbing #249 [Live @ Zouk, Singapore].mp3'; 24 | expect(actual).toBe(expected); 25 | }); 26 | test('parseTracklist_1', () => { 27 | const value = '\ 28 | 02:41 Bobina - Miami "Echoes"'; 29 | const actual = parser.parseTrackList(value); 30 | const expected = [ 31 | { 32 | performer: 'Bobina', 33 | time: '02:41:00', 34 | title: `Miami 'Echoes'`, 35 | track: 1, 36 | }, 37 | ]; 38 | expect(actual).toStrictEqual(expected); 39 | }); 40 | test('parseTracklist_2', () => { 41 | const value = '\ 42 | 02:41 Miami "Echoes"'; 43 | const actual = parser.parseTrackList(value); 44 | const expected = [ 45 | { 46 | performer: '', 47 | time: '02:41:00', 48 | title: `Miami 'Echoes'`, 49 | track: 1, 50 | }, 51 | ]; 52 | expect(actual).toStrictEqual(expected); 53 | }); 54 | test('parseRegionsList', () => { 55 | const regionsList: { [key: string]: string } = { 56 | ' Marker 06 01:10:38:52': '70:38:52', 57 | ' 22 02:01:50.04': '121:50:04', 58 | ' 22 02:01:50,04': '121:50:04', 59 | ' 5541.293333 7143.640000 19': '92:21:21', 60 | ' 50:10:01 \n': '50:10:01', 61 | '01 120:10.01 (Split)': '120:10:01', 62 | 'Marker 02 0:09.623': '00:09:46', 63 | }; 64 | 65 | Object.keys(regionsList).forEach((key: string) => { 66 | const actual = parser.parseTimings(key); 67 | const expected = regionsList[key]; 68 | expect(timeEntryToString(actual[0])).toBe(expected); 69 | }); 70 | }); 71 | }); 72 | 73 | describe('ParserHelper', () => { 74 | test('splitTitlePerformer', () => { 75 | const value = "02:41 Dinka - Elements (EDX's 5un5hine Remix)"; 76 | const actual = parserHelper.splitTitlePerformer(value); 77 | const expected = { 78 | performer: '02:41 Dinka', 79 | title: "Elements (EDX's 5un5hine Remix)", 80 | }; 81 | expect(actual).toStrictEqual(expected); 82 | }); 83 | test('splitTitlePerformerTitleOnly', () => { 84 | const value = "02:41 Dinka Elements (EDX's 5un5hine Remix)"; 85 | const actual = parserHelper.splitTitlePerformer(value); 86 | const expected = { 87 | performer: '', 88 | title: "02:41 Dinka Elements (EDX's 5un5hine Remix)", 89 | }; 90 | expect(actual).toStrictEqual(expected); 91 | }); 92 | test('removeDoubleQuotes', () => { 93 | const value = 'Elements (EDX "5un5hine" Remix)'; 94 | const actual = parserHelper.removeDoubleQuotes(value); 95 | const expected = 'Elements (EDX 5un5hine Remix)'; 96 | expect(actual).toBe(expected); 97 | }); 98 | test('replaceDoubleQuotes', () => { 99 | const value = `Elements (EDX "5un5hine" Remix)`; 100 | const actual = parserHelper.replaceDoubleQuotes(value); 101 | const expected = `Elements (EDX '5un5hine' Remix)`; 102 | expect(actual).toBe(expected); 103 | }); 104 | test('separateTime', () => { 105 | const timePerformers: { [key: string]: { time: string; residue: string } } = { 106 | '[08:45] 03. 8 Ball': { 107 | time: '08:45', 108 | residue: '[] 03. 8 Ball', 109 | }, 110 | '01.[18:02] Giuseppe': { 111 | time: '18:02', 112 | residue: '01.[] Giuseppe', 113 | }, 114 | '10:57 02. Space Manoeuvres': { 115 | time: '10:57', 116 | residue: '02. Space Manoeuvres', 117 | }, 118 | ' CJ Bolland ': { 119 | time: '', 120 | residue: 'CJ Bolland', 121 | }, 122 | '04 Mr. Fluff': { 123 | time: '', 124 | residue: '04 Mr. Fluff', 125 | }, 126 | '9999:53 T.O.M': { 127 | time: '9999:53', 128 | residue: 'T.O.M', 129 | }, 130 | '999:02:28 Mossy': { 131 | time: '999:02:28', 132 | residue: 'Mossy', 133 | }, 134 | '2:28 NO LEADING ZERO': { 135 | time: '2:28', 136 | residue: 'NO LEADING ZERO', 137 | }, 138 | 'School 1:42': { 139 | time: '1:42', 140 | residue: 'School', 141 | }, 142 | }; 143 | Object.keys(timePerformers).forEach((key) => { 144 | const actual = parserHelper.separateTime(key); 145 | const actualTime = actual.time; 146 | const actualPerformer = actual.residue; 147 | 148 | const expectedTime = timePerformers[key].time; 149 | const expectedPerformer = timePerformers[key].residue; 150 | 151 | expect(actualTime).toBe(expectedTime); 152 | expect(actualPerformer).toBe(expectedPerformer); 153 | }); 154 | }); 155 | test('cleanOffTime', () => { 156 | const performers: { [key: string]: string } = { 157 | '] Giuseppe': 'Giuseppe', 158 | '02. Space Manoeuvres': 'Space Manoeuvres', 159 | '04 Mr. Fluff': 'Mr. Fluff', 160 | 'CJ Bolland': 'CJ Bolland', 161 | '08) CJ Bolland': 'CJ Bolland', 162 | '] 03. 8 Ball': '8 Ball', 163 | }; 164 | Object.keys(performers).forEach((key) => { 165 | const actual = parserHelper.cleanOffTime(key); 166 | const expected = performers[key]; 167 | expect(actual).toBe(expected); 168 | }); 169 | }); 170 | test('castTime', () => { 171 | const times: { [key: string]: string } = { 172 | // h:m:s|m:s -> m:s:f 173 | '999:02:28': '59942:28:00', 174 | '9999:53': '9999:53:00', 175 | '3:28': '03:28:00', 176 | '1:02:28': '62:28:00', 177 | '56:63': '56:63:00', 178 | '246:10': '246:10:00', 179 | '': '00:00:00', 180 | '00:05:12': '05:12:00', 181 | }; 182 | 183 | Object.keys(times).forEach(function (key) { 184 | const actual = parserHelper.castTime(key); 185 | const expected = times[key]; 186 | expect(actual).toBe(expected); 187 | }); 188 | }); 189 | }); 190 | -------------------------------------------------------------------------------- /src/Cue/Parser.ts: -------------------------------------------------------------------------------- 1 | import { Timings, TimeEntry, TrackList } from '../types'; 2 | import { timeEntryToString } from './Formatter'; 3 | import { adobeAudition, audacity, soundforge, standard, winampNero } from './TimingParsers'; 4 | 5 | export class ParserHelper { 6 | // that's how we tell performer and title apart 7 | private titlePerformerSeparators = [ 8 | ' - ', // 45 hyphen-minus 9 | ' – ', // 8211 en dash 10 | ' ‒ ', // 8210 figure dash 11 | ' — ', // 8212 em dash 12 | ' ― ', // 8213 horizontal bar 13 | ]; 14 | splitTitlePerformer(value: string) { 15 | // `foreach` and `switch` are toooooooooo slow! 16 | let split = [], 17 | performer = '', 18 | title = ''; 19 | 20 | if (-1 !== value.search(this.titlePerformerSeparators[0])) { 21 | split = value.split(this.titlePerformerSeparators[0]); 22 | } else if (-1 !== value.search(this.titlePerformerSeparators[1])) { 23 | split = value.split(this.titlePerformerSeparators[1]); 24 | } else if (-1 !== value.search(this.titlePerformerSeparators[2])) { 25 | split = value.split(this.titlePerformerSeparators[2]); 26 | } else if (-1 !== value.search(this.titlePerformerSeparators[3])) { 27 | split = value.split(this.titlePerformerSeparators[3]); 28 | } else if (-1 !== value.search(this.titlePerformerSeparators[4])) { 29 | split = value.split(this.titlePerformerSeparators[4]); 30 | } else { 31 | split = [value]; 32 | } 33 | 34 | // if string wasn't split yet then we get just a title (performer assumed to be the global one) 35 | if (1 === split.length) { 36 | performer = ''; 37 | title = split.shift() || ''; 38 | } else { 39 | performer = split.shift() || ''; 40 | title = split.join(' '); 41 | } 42 | 43 | return { 44 | performer: performer.trim(), 45 | title: title.trim(), 46 | }; 47 | } 48 | 49 | separateTime(value: string) { 50 | var time = '', 51 | residue = ''; 52 | 53 | // ask to increase minutes up to 9999 referred to the "h:m" not "h:m:f" format 54 | // but I still increased "h:m:f" up to 999 hours just in case 55 | // https://github.com/dVaffection/cuegenerator/issues/14 56 | 57 | // 01. 9999:53 | 999:02:28 58 | var pattern = /(?:\d{2}\.)?\[?((?:\d{1,3}:)?\d{1,4}:\d{2})\]?.*/i; 59 | var matches = value.match(pattern); 60 | 61 | if (matches && matches[1]) { 62 | time = matches[1].trim(); 63 | // residue = value.substring(value.indexOf(matches[1]) + matches[1].length).trim(); 64 | residue = value.replace(time, '').trim(); 65 | } else { 66 | residue = value.trim(); 67 | } 68 | 69 | return { 70 | time: time, 71 | residue: residue, 72 | }; 73 | } 74 | 75 | /** 76 | * Accept time in format either hr:mn:sc or mn:sc 77 | * 78 | * @param {String} 79 | * @returns mn:sc:fr 80 | */ 81 | castTime(value: string) { 82 | let castTime = '00:00:00'; 83 | value = value.trim(); 84 | 85 | const pattern = /^\d{1,4}:\d{2}:\d{2}$/; 86 | const matches = value.match(pattern); 87 | if (matches) { 88 | const times = value.split(':'); 89 | const hrParsed = parseInt(times[0], 10); 90 | const mnParsed = parseInt(times[1], 10); 91 | const sc = times[2].padStart(2, '0'); 92 | const mn = String(hrParsed * 60 + mnParsed).padStart(2, '0'); 93 | castTime = mn + ':' + sc + ':00'; 94 | } else { 95 | const pattern = /(^\d{1,4}):(\d{2})$/; 96 | const matches = value.match(pattern); 97 | if (matches) { 98 | const mn = matches[1].padStart(2, '0'); 99 | const sc = matches[2].padStart(2, '0'); 100 | castTime = `${mn}:${sc}:00`; 101 | } 102 | } 103 | 104 | return castTime; 105 | } 106 | 107 | cleanOffTime(value: string) { 108 | var pattern = /^(?:\]? )?(?:\d{2}\)?\.? )?(.*)$/i; 109 | var matches = value.match(pattern); 110 | 111 | if (matches && matches[1]) { 112 | value = matches[1]; 113 | } 114 | 115 | return value; 116 | } 117 | 118 | removeDoubleQuotes(value: string) { 119 | return value.replace(/"/g, ''); 120 | } 121 | 122 | replaceDoubleQuotes(value: string) { 123 | return value.replace(/"/g, "'"); 124 | } 125 | } 126 | 127 | export default class Parser { 128 | static readonly regionsListParsers = [adobeAudition, audacity, soundforge, winampNero, standard]; 129 | 130 | constructor(readonly helper: ParserHelper) {} 131 | 132 | parsePerformer(v: string): string { 133 | return v.trim(); 134 | } 135 | parseTitle(v: string): string { 136 | return v.trim(); 137 | } 138 | parseFileName(v: string): string { 139 | return v.trim(); 140 | } 141 | parseTrackList(value: string): TrackList { 142 | const trackList = []; 143 | let time, performer, title; 144 | 145 | const contentInLines = value.split('\n'); 146 | for (let i = 0, track = 1; i < contentInLines.length; i++, track++) { 147 | const row = contentInLines[i].trim(); 148 | if (!row.length) { 149 | track--; 150 | continue; 151 | } 152 | 153 | const performerTitle = this.helper.splitTitlePerformer(row); 154 | 155 | if (performerTitle.performer) { 156 | const timePerformer = this.helper.separateTime(performerTitle.performer); 157 | time = this.helper.castTime(timePerformer.time); 158 | performer = this.helper.cleanOffTime(timePerformer.residue); 159 | title = performerTitle.title; 160 | } else { 161 | performer = ''; 162 | const timeTitle = this.helper.separateTime(performerTitle.title); 163 | time = this.helper.castTime(timeTitle.time); 164 | title = this.helper.cleanOffTime(timeTitle.residue); 165 | } 166 | 167 | performer = this.helper.replaceDoubleQuotes(performer); 168 | title = this.helper.replaceDoubleQuotes(title); 169 | 170 | trackList.push({ 171 | track, 172 | performer, 173 | title, 174 | time, 175 | }); 176 | } 177 | 178 | return trackList; 179 | } 180 | 181 | parseTimings(value: string): Timings { 182 | const contents = value.split('\n'); 183 | 184 | // define a parser 185 | let regionsListParser: typeof Parser.regionsListParsers[0] | undefined; 186 | for (const parser of Parser.regionsListParsers) { 187 | const timeEntry = parser(contents[0] || ''); 188 | if (timeEntry) { 189 | regionsListParser = parser; 190 | break; 191 | } 192 | } 193 | 194 | if (regionsListParser === undefined) return []; 195 | 196 | // apply the parser for every line 197 | return contents 198 | .map((row) => regionsListParser!(row)) 199 | .filter((timeEntry): timeEntry is TimeEntry => timeEntry !== undefined); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Components/Form.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import _ from 'lodash'; 4 | import './Form.css'; 5 | import FormSelect from './Form/FormSelect'; 6 | import { formHandler } from '../Cue'; 7 | import { api, analytics, cueStorage } from '../Services'; 8 | import CounterContext from './CounterContext'; 9 | import { makeCueFileName } from '../Utils'; 10 | import { AnalyticsEvent } from '../Services/Analytics'; 11 | import { CueFormInputs } from '../types'; 12 | 13 | interface FORM_STATE_TYPE extends CueFormInputs { 14 | cue: string; 15 | } 16 | 17 | // type FORM_STATE_TYPE = { 18 | // performer: string; 19 | // title: string; 20 | // fileName: string; 21 | // fileType: string; 22 | // trackList: string; 23 | // regionsList: string; 24 | // cue: string; 25 | // }; 26 | 27 | export default function Form() { 28 | const fileTypes = ['MP3', 'AAC', 'AIFF', 'ALAC', 'BINARY', 'FLAC', 'MOTOROLA', 'WAVE']; 29 | const FORM_INIT_STATE = { 30 | performer: '', 31 | title: '', 32 | fileName: '', 33 | fileType: fileTypes[0], 34 | trackList: '', 35 | regionsList: '', 36 | cue: '', 37 | }; 38 | 39 | const [formState, setFormState] = useState