···1818import { UpstreamError } from "../../errors/UpstreamError.ts";
1919import type { ComAtprotoRepoCreateRecord, ComAtprotoRepoPutRecord } from '@atcute/atproto';
2020import { nowPlayingExpirationDuration } from "../../../scrobblers/AbstractScrobbleClient.ts";
2121+import { isrcNoHyphens } from "../../../../core/PlayUtils.ts";
21222223export class TealApiClient extends AbstractApiClient implements PagelessTimeRangeListens {
2324···196197 releaseName: play.data.album,
197198 submissionClientAgent: `multi-scrobbler/${getRoot().items.version}`,
198199 musicServiceBaseDomain: musicServiceToCononical(play.meta.musicService) ?? play.meta.musicService,
199199- isrc: play.data.isrc,
200200+ isrc: play.data.isrc !== undefined ? isrcNoHyphens(play.data.isrc) : undefined,
200201 trackMbId: mbidUriOrUndefined(play.data.meta?.brainz?.track as MBID),
201202 recordingMbId: mbidUriOrUndefined(play.data.meta?.brainz?.recording as MBID),
202203 releaseMbId: mbidUriOrUndefined(play.data.meta?.brainz?.album as MBID)
+85
src/backend/tests/utils/strings.test.ts
···77import { replaceInterpolatedValues } from "../../utils/DataUtils.ts";
88import { splitByFirstFound } from '../../../core/StringUtils.ts';
99import { noCasePropObj } from '../../utils/DataUtils.ts';
1010+import { isrcNoHyphens, isrcWithHyphens, REGEX_ISRC_HYPHENS, REGEX_ISRC_NO_HYPHENS } from '../../../core/PlayUtils.ts';
10111112describe('String Comparisons', function () {
1213···172173 });
173174});
174175176176+describe('ISRC Parsing', function() {
177177+178178+ describe('Regex tests', function() {
179179+180180+ it('isrc no-hypen regex matches isrc with no hyphens', function() {
181181+ const isrcs = ['USRC17607839','GBWUL1805639','USUG11902962','QT65X2609829'];
182182+ for(const i of isrcs) {
183183+ expect(REGEX_ISRC_NO_HYPHENS.test(i)).is.true;
184184+ }
185185+ });
186186+187187+ it('isrc no-hypen regex does not match isrc with hyphens or invalid', function() {
188188+ const isrcs = ['US-RC1-76-07839','GB-WUL1-80-5639','US-UG1-19-02962','QTLASDSDASD',''];
189189+ for(const i of isrcs) {
190190+ expect(REGEX_ISRC_NO_HYPHENS.test(i)).is.false;
191191+ }
192192+ });
193193+194194+ it('isrc hypen regex matches isrc with hyphens', function() {
195195+ const isrcs = ['US-RC1-76-07839','GB-WUL-18-05639','US-UG1-19-02962'];
196196+ for(const i of isrcs) {
197197+ expect(REGEX_ISRC_HYPHENS.test(i), `${i} did not match`).is.true;
198198+ }
199199+ });
200200+201201+ it('isrc hypen regex does not match isrc with no hyphens or invalid', function() {
202202+ const isrcs = ['USRC17607839','GBWUL1805639','USUG11902962','QT65X2609829','QTLASDSDASD',''];
203203+ for(const i of isrcs) {
204204+ expect(REGEX_ISRC_HYPHENS.test(i)).is.false;
205205+ }
206206+ });
207207+208208+ });
209209+210210+ describe('ISRC Transform', function() {
211211+212212+ describe('To non-hyphenated', function() {
213213+ it('Transforms from hyphenated', function() {
214214+ const isrcs = ['US-RC1-76-07839','GB-WUL-18-05639','US-UG1-19-02962'];
215215+ for(const i of isrcs) {
216216+ const res = isrcNoHyphens(i);
217217+ expect(res).eq(i.replaceAll(/-/g, ''));
218218+ }
219219+ });
220220+221221+ it('Returns already non-hyphenated', function() {
222222+ const isrcs = ['USRC17607839','GBWUL1805639','USUG11902962','QT65X2609829'];
223223+ for(const i of isrcs) {
224224+ const res = isrcNoHyphens(i);
225225+ expect(res).eq(i);
226226+ }
227227+ });
228228+229229+ it('Fails on invalid', function() {
230230+ const isrcs = ['GB-WUL1-80-5639','QTLASDSDASD',''];
231231+ for(const i of isrcs) {
232232+ expect(() => isrcNoHyphens(i)).to.throw();
233233+ }
234234+ });
235235+ });
236236+237237+ describe('To hyphenated', function() {
238238+ it('Transforms from non-hyphenated', function() {
239239+ const res = isrcWithHyphens('USRC17607839');
240240+ expect(res).eq('US-RC1-76-07839');
241241+ });
242242+243243+ it('Returns already non-hyphenated', function() {
244244+ const isrcs = ['US-RC1-76-07839','GB-WUL-18-05639','US-UG1-19-02962'];
245245+ for(const i of isrcs) {
246246+ const res = isrcWithHyphens(i);
247247+ expect(res).eq(i);
248248+ }
249249+ });
250250+251251+ it('Fails on invalid', function() {
252252+ const isrcs = ['GB-WUL1-80-5639','QTLASDSDASD',''];
253253+ for(const i of isrcs) {
254254+ expect(() => isrcNoHyphens(i)).to.throw();
255255+ }
256256+ });
257257+ });
258258+ });
259259+});
+50
src/core/PlayUtils.ts
···11+import { parseRegexSingle } from "@foxxmd/regex-buddy-core";
12import type {AmbPlayObject, DateLike, PlayObject, PlayObjectMinimal, PlayPlatformId} from "./Atomic.ts";
23import dayjs from "dayjs";
34···5657 };
5758};
58596060+export const REGEX_ISRC_NO_HYPHENS = new RegExp(/^(?<cc>[\d\w]{2})(?<registrant>[\d\w]{3})(?<year>\d{2})(?<designation>\d{5})$/);
6161+export const REGEX_ISRC_HYPHENS = new RegExp(/^(?<cc>[\d\w]{2})-(?<registrant>[\d\w]{3})-(?<year>\d{2})-(?<designation>\d{5})$/);
6262+6363+/**
6464+ * Removes hyphens from ISRC identifiers
6565+ *
6666+ * ISRC identifiers officially use hyphens but it is valid to use them without
6767+ * and some systems expect no hyphens (musicbrainz)
6868+ *
6969+ * @see https://en.wikipedia.org/wiki/International_Standard_Recording_Code
7070+ * @see https://isrctools.com/format/
7171+ */
7272+export const isrcNoHyphens = (isrc: string): string => {
7373+ const parsed = parseRegexSingle(REGEX_ISRC_HYPHENS, isrc);
7474+ if(parsed === undefined) {
7575+ // check if its already parsed
7676+ const alreadyOk = REGEX_ISRC_NO_HYPHENS.test(isrc);
7777+ if(alreadyOk) {
7878+ return isrc;
7979+ }
8080+ throw new Error(`Value ${isrc} is not a valid ISRC`);
8181+ }
8282+ return `${parsed.named.cc}${parsed.named.registrant}${parsed.named.year}${parsed.named.designation}`;
8383+}
8484+8585+/**
8686+ * Add hyphens to ISRC identifiers
8787+ *
8888+ * ISRC identifiers officially use hyphens but it is valid to use them without
8989+ * and some systems expect no hyphens (musicbrainz).
9090+ *
9191+ * This function re-adds hyphens to an isrc value if they are not present
9292+ *
9393+ * @see https://en.wikipedia.org/wiki/International_Standard_Recording_Code
9494+ * @see https://isrctools.com/format/
9595+ *
9696+ */
9797+export const isrcWithHyphens = (isrc: string): string => {
9898+ const parsed = parseRegexSingle(REGEX_ISRC_NO_HYPHENS, isrc);
9999+ if(parsed === undefined) {
100100+ // check if its already parsed
101101+ const alreadyOk = REGEX_ISRC_HYPHENS.test(isrc);
102102+ if(alreadyOk) {
103103+ return isrc;
104104+ }
105105+ throw new Error(`Value ${isrc} is not a valid ISRC`);
106106+ }
107107+ return `${parsed.named.cc}-${parsed.named.registrant}-${parsed.named.year}-${parsed.named.designation}`;
108108+}