✨ mostly complete testing, preparing for data type change
This commit is contained in:
parent
2178137c29
commit
dd06c5ac65
|
|
@ -9,7 +9,7 @@ export const ThumbnailFull = type({
|
||||||
|
|
||||||
export type ArtistBasic = typeof ArtistBasic.infer
|
export type ArtistBasic = typeof ArtistBasic.infer
|
||||||
export const ArtistBasic = type({
|
export const ArtistBasic = type({
|
||||||
artistId: "string",
|
artistId: "string|null",
|
||||||
name: "string",
|
name: "string",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -120,7 +120,6 @@ export const AlbumFull = type({
|
||||||
artists: [ArtistBasic, "[]"],
|
artists: [ArtistBasic, "[]"],
|
||||||
year: "number|null",
|
year: "number|null",
|
||||||
thumbnails: [ThumbnailFull, "[]"],
|
thumbnails: [ThumbnailFull, "[]"],
|
||||||
description: "string",
|
|
||||||
songs: [SongDetailed, "[]"],
|
songs: [SongDetailed, "[]"],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -360,7 +360,12 @@ export default class YTMusic {
|
||||||
const lyricsData = await this.constructRequest("browse", { browseId })
|
const lyricsData = await this.constructRequest("browse", { browseId })
|
||||||
const lyrics = traverseString(lyricsData, "description", "runs", "text")()
|
const lyrics = traverseString(lyricsData, "description", "runs", "text")()
|
||||||
|
|
||||||
return lyrics ? lyrics.replaceAll("\r", "").split("\n") : null
|
return lyrics
|
||||||
|
? lyrics
|
||||||
|
.replaceAll("\r", "")
|
||||||
|
.split("\n")
|
||||||
|
.filter(v => !!v)
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,14 @@ export default class AlbumParser {
|
||||||
albumId,
|
albumId,
|
||||||
name: traverseString(data, "header", "title", "text")(),
|
name: traverseString(data, "header", "title", "text")(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const artists: ArtistBasic[] = traverseList(data, "header", "subtitle", "runs")
|
const artists: ArtistBasic[] = traverseList(data, "header", "subtitle", "runs")
|
||||||
.filter(run => "navigationEndpoint" in run)
|
.filter(run => "navigationEndpoint" in run)
|
||||||
.map(run => ({
|
.map(run => ({
|
||||||
artistId: traverseString(run, "browseId")(),
|
artistId: traverseString(run, "browseId")(),
|
||||||
name: traverseString(run, "text")(),
|
name: traverseString(run, "text")(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const thumbnails = traverseList(data, "header", "thumbnails")
|
const thumbnails = traverseList(data, "header", "thumbnails")
|
||||||
|
|
||||||
return checkType(
|
return checkType(
|
||||||
|
|
@ -28,7 +30,6 @@ export default class AlbumParser {
|
||||||
traverseString(data, "header", "subtitle", "text")(-1),
|
traverseString(data, "header", "subtitle", "text")(-1),
|
||||||
),
|
),
|
||||||
thumbnails,
|
thumbnails,
|
||||||
description: traverseString(data, "description", "text")(),
|
|
||||||
songs: traverseList(data, "musicResponsiveListItemRenderer").map(item =>
|
songs: traverseList(data, "musicResponsiveListItemRenderer").map(item =>
|
||||||
SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails),
|
SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ArtistBasic, ArtistDetailed, ArtistFull } from "../@types/types"
|
import { ArtistDetailed, ArtistFull } from "../@types/types"
|
||||||
import checkType from "../utils/checkType"
|
import checkType from "../utils/checkType"
|
||||||
import traverseList from "../utils/traverseList"
|
import traverseList from "../utils/traverseList"
|
||||||
import traverseString from "../utils/traverseString"
|
import traverseString from "../utils/traverseString"
|
||||||
|
|
@ -9,7 +9,7 @@ import VideoParser from "./VideoParser"
|
||||||
|
|
||||||
export default class ArtistParser {
|
export default class ArtistParser {
|
||||||
public static parse(data: any, artistId: string): ArtistFull {
|
public static parse(data: any, artistId: string): ArtistFull {
|
||||||
const artistBasic: ArtistBasic = {
|
const artistBasic = {
|
||||||
artistId,
|
artistId,
|
||||||
name: traverseString(data, "header", "title", "text")(),
|
name: traverseString(data, "header", "title", "text")(),
|
||||||
}
|
}
|
||||||
|
|
@ -58,13 +58,16 @@ export default class ArtistParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseSearchResult(item: any): ArtistDetailed {
|
public static parseSearchResult(item: any): ArtistDetailed {
|
||||||
const flexColumns = traverseList(item, "flexColumns")
|
const columns = traverseList(item, "flexColumns")
|
||||||
|
|
||||||
|
// No specific way to identify the title
|
||||||
|
const title = columns[0]
|
||||||
|
|
||||||
return checkType(
|
return checkType(
|
||||||
{
|
{
|
||||||
type: "ARTIST",
|
type: "ARTIST",
|
||||||
artistId: traverseString(item, "browseId")(),
|
artistId: traverseString(item, "browseId")(),
|
||||||
name: traverseString(flexColumns[0], "runs", "text")(),
|
name: traverseString(title, "runs", "text")(),
|
||||||
thumbnails: traverseList(item, "thumbnails"),
|
thumbnails: traverseList(item, "thumbnails"),
|
||||||
},
|
},
|
||||||
ArtistDetailed,
|
ArtistDetailed,
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
import { PlaylistDetailed, PlaylistFull } from "../@types/types"
|
import { PlaylistDetailed, PlaylistFull } from "../@types/types"
|
||||||
import checkType from "../utils/checkType"
|
import checkType from "../utils/checkType"
|
||||||
|
import { isArtist } from "../utils/filters"
|
||||||
|
import traverse from "../utils/traverse"
|
||||||
import traverseList from "../utils/traverseList"
|
import traverseList from "../utils/traverseList"
|
||||||
import traverseString from "../utils/traverseString"
|
import traverseString from "../utils/traverseString"
|
||||||
|
|
||||||
export default class PlaylistParser {
|
export default class PlaylistParser {
|
||||||
public static parse(data: any, playlistId: string): PlaylistFull {
|
public static parse(data: any, playlistId: string): PlaylistFull {
|
||||||
|
const artist = traverse(data, "header", "subtitle")
|
||||||
|
|
||||||
return checkType(
|
return checkType(
|
||||||
{
|
{
|
||||||
type: "PLAYLIST",
|
type: "PLAYLIST",
|
||||||
playlistId,
|
playlistId,
|
||||||
name: traverseString(data, "header", "title", "text")(),
|
name: traverseString(data, "header", "title", "text")(),
|
||||||
artist: {
|
artist: {
|
||||||
artistId: traverseString(data, "header", "subtitle", "browseId")(),
|
name: traverseString(artist, "text")(),
|
||||||
name: traverseString(data, "header", "subtitle", "text")(2),
|
artistId: traverseString(artist, "browseId")(),
|
||||||
},
|
},
|
||||||
videoCount:
|
videoCount:
|
||||||
+traverseList(data, "header", "secondSubtitle", "text")
|
+traverseList(data, "header", "secondSubtitle", "text")
|
||||||
|
|
@ -27,16 +31,21 @@ export default class PlaylistParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseSearchResult(item: any): PlaylistDetailed {
|
public static parseSearchResult(item: any): PlaylistDetailed {
|
||||||
const flexColumns = traverseList(item, "flexColumns")
|
const columns = traverseList(item, "flexColumns", "runs").flat()
|
||||||
|
|
||||||
|
// No specific way to identify the title
|
||||||
|
const title = columns[0]
|
||||||
|
// Possibility to be empty because it's by YouTube Music
|
||||||
|
const artist = columns.find(isArtist)
|
||||||
|
|
||||||
return checkType(
|
return checkType(
|
||||||
{
|
{
|
||||||
type: "PLAYLIST",
|
type: "PLAYLIST",
|
||||||
playlistId: traverseString(item, "overlay", "playlistId")(),
|
playlistId: traverseString(item, "overlay", "playlistId")(),
|
||||||
name: traverseString(flexColumns[0], "runs", "text")(),
|
name: traverseString(title, "text")(),
|
||||||
artist: {
|
artist: {
|
||||||
artistId: traverseString(flexColumns[1], "browseId")(),
|
name: artist ? traverseString(artist, "text")() : "YouTube Music",
|
||||||
name: traverseString(flexColumns[1], "runs", "text")(-3),
|
artistId: traverseString(artist, "browseId")() || null,
|
||||||
},
|
},
|
||||||
thumbnails: traverseList(item, "thumbnails"),
|
thumbnails: traverseList(item, "thumbnails"),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { AlbumBasic, ArtistBasic, SongDetailed, SongFull, ThumbnailFull } from "../@types/types"
|
import { AlbumBasic, ArtistBasic, SongDetailed, SongFull, ThumbnailFull } from "../@types/types"
|
||||||
import checkType from "../utils/checkType"
|
import checkType from "../utils/checkType"
|
||||||
|
import { isAlbum, isArtist, isDuration, isTitle } from "../utils/filters"
|
||||||
import traverseList from "../utils/traverseList"
|
import traverseList from "../utils/traverseList"
|
||||||
import traverseString from "../utils/traverseString"
|
import traverseString from "../utils/traverseString"
|
||||||
import Parser from "./Parser"
|
import Parser from "./Parser"
|
||||||
|
|
@ -28,25 +29,29 @@ export default class SongParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseSearchResult(item: any): SongDetailed {
|
public static parseSearchResult(item: any): SongDetailed {
|
||||||
const flexColumns = traverseList(item, "flexColumns")
|
const columns = traverseList(item, "flexColumns", "runs").flat()
|
||||||
|
|
||||||
|
const title = columns.find(isTitle)
|
||||||
|
const artist = columns.find(isArtist)
|
||||||
|
const album = columns.find(isAlbum)
|
||||||
|
const duration = columns.find(isDuration)
|
||||||
|
|
||||||
return checkType(
|
return checkType(
|
||||||
{
|
{
|
||||||
type: "SONG",
|
type: "SONG",
|
||||||
videoId: traverseString(item, "playlistItemData", "videoId")(),
|
videoId: traverseString(item, "playlistItemData", "videoId")(),
|
||||||
name: traverseString(flexColumns[0], "runs", "text")(),
|
name: traverseString(title, "text")(),
|
||||||
artists: traverseList(flexColumns[1], "runs")
|
artists: [
|
||||||
.filter(run => "navigationEndpoint" in run)
|
{
|
||||||
.map(run => ({
|
name: traverseString(artist, "text")(),
|
||||||
artistId: traverseString(run, "browseId")(),
|
artistId: traverseString(artist, "browseId")(),
|
||||||
name: traverseString(run, "text")(),
|
},
|
||||||
}))
|
],
|
||||||
.slice(0, -1),
|
|
||||||
album: {
|
album: {
|
||||||
albumId: traverseString(flexColumns[1], "runs", "browseId")(-1),
|
name: traverseString(album, "text")(),
|
||||||
name: traverseString(flexColumns[1], "runs", "text")(-3),
|
albumId: traverseString(album, "browseId")(),
|
||||||
},
|
},
|
||||||
duration: Parser.parseDuration(traverseString(flexColumns[1], "runs", "text")(-1)),
|
duration: Parser.parseDuration(duration.text),
|
||||||
thumbnails: traverseList(item, "thumbnails"),
|
thumbnails: traverseList(item, "thumbnails"),
|
||||||
},
|
},
|
||||||
SongDetailed,
|
SongDetailed,
|
||||||
|
|
@ -54,27 +59,29 @@ export default class SongParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseArtistSong(item: any): SongDetailed {
|
public static parseArtistSong(item: any): SongDetailed {
|
||||||
const flexColumns = traverseList(item, "flexColumns")
|
const columns = traverseList(item, "flexColumns", "runs").flat()
|
||||||
const videoId = traverseString(item, "playlistItemData", "videoId")()
|
|
||||||
|
const title = columns.find(isTitle)
|
||||||
|
const artist = columns.find(isArtist)
|
||||||
|
const album = columns.find(isAlbum)
|
||||||
|
const duration = columns.find(isDuration)
|
||||||
|
|
||||||
return checkType(
|
return checkType(
|
||||||
{
|
{
|
||||||
type: "SONG",
|
type: "SONG",
|
||||||
videoId,
|
videoId: traverseString(item, "playlistItemData", "videoId")(),
|
||||||
name: traverseString(flexColumns[0], "runs", "text")(),
|
name: traverseString(title, "text")(),
|
||||||
artists: traverseList(flexColumns[1], "runs")
|
artists: [
|
||||||
.filter(run => "navigationEndpoint" in run)
|
{
|
||||||
.map(run => ({
|
name: traverseString(artist, "text")(),
|
||||||
artistId: traverseString(run, "browseId")(),
|
artistId: traverseString(artist, "browseId")(),
|
||||||
name: traverseString(run, "text")(),
|
},
|
||||||
})),
|
],
|
||||||
album: {
|
album: {
|
||||||
albumId: traverseString(flexColumns[2], "browseId")(),
|
name: traverseString(album, "text")(),
|
||||||
name: traverseString(flexColumns[2], "runs", "text")(),
|
albumId: traverseString(album, "browseId")(),
|
||||||
},
|
},
|
||||||
duration: Parser.parseDuration(
|
duration: duration ? Parser.parseDuration(duration.text) : null,
|
||||||
traverseString(item, "fixedColumns", "runs", "text")(),
|
|
||||||
),
|
|
||||||
thumbnails: traverseList(item, "thumbnails"),
|
thumbnails: traverseList(item, "thumbnails"),
|
||||||
},
|
},
|
||||||
SongDetailed,
|
SongDetailed,
|
||||||
|
|
@ -82,18 +89,20 @@ export default class SongParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseArtistTopSong(item: any, artistBasic: ArtistBasic): SongDetailed {
|
public static parseArtistTopSong(item: any, artistBasic: ArtistBasic): SongDetailed {
|
||||||
const flexColumns = traverseList(item, "flexColumns")
|
const columns = traverseList(item, "flexColumns", "runs").flat()
|
||||||
const videoId = traverseString(item, "playlistItemData", "videoId")()
|
|
||||||
|
const title = columns.find(isTitle)
|
||||||
|
const album = columns.find(isAlbum)
|
||||||
|
|
||||||
return checkType(
|
return checkType(
|
||||||
{
|
{
|
||||||
type: "SONG",
|
type: "SONG",
|
||||||
videoId,
|
videoId: traverseString(item, "playlistItemData", "videoId")(),
|
||||||
name: traverseString(flexColumns[0], "runs", "text")(),
|
name: traverseString(title, "text")(),
|
||||||
artists: [artistBasic],
|
artists: [artistBasic],
|
||||||
album: {
|
album: {
|
||||||
albumId: traverseString(flexColumns[2], "browseId")(),
|
name: traverseString(album, "text")(),
|
||||||
name: traverseString(flexColumns[2], "runs", "text")(),
|
albumId: traverseString(album, "browseId")(),
|
||||||
},
|
},
|
||||||
duration: null,
|
duration: null,
|
||||||
thumbnails: traverseList(item, "thumbnails"),
|
thumbnails: traverseList(item, "thumbnails"),
|
||||||
|
|
@ -108,19 +117,19 @@ export default class SongParser {
|
||||||
albumBasic: AlbumBasic,
|
albumBasic: AlbumBasic,
|
||||||
thumbnails: ThumbnailFull[],
|
thumbnails: ThumbnailFull[],
|
||||||
): SongDetailed {
|
): SongDetailed {
|
||||||
const flexColumns = traverseList(item, "flexColumns")
|
const columns = traverseList(item, "flexColumns", "runs").flat()
|
||||||
const videoId = traverseString(item, "playlistItemData", "videoId")()
|
|
||||||
|
const title = columns.find(isTitle)
|
||||||
|
const duration = columns.find(isDuration)
|
||||||
|
|
||||||
return checkType(
|
return checkType(
|
||||||
{
|
{
|
||||||
type: "SONG",
|
type: "SONG",
|
||||||
videoId,
|
videoId: traverseString(item, "playlistItemData", "videoId")(),
|
||||||
name: traverseString(flexColumns[0], "runs", "text")(),
|
name: traverseString(title, "text")(),
|
||||||
artists,
|
artists,
|
||||||
album: albumBasic,
|
album: albumBasic,
|
||||||
duration: Parser.parseDuration(
|
duration: duration ? Parser.parseDuration(duration.text) : null,
|
||||||
traverseString(item, "fixedColumns", "runs", "text")(),
|
|
||||||
),
|
|
||||||
thumbnails,
|
thumbnails,
|
||||||
},
|
},
|
||||||
SongDetailed,
|
SongDetailed,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { ArtistBasic, VideoDetailed, VideoFull } from "../@types/types"
|
import { ArtistBasic, VideoDetailed, VideoFull } from "../@types/types"
|
||||||
import checkType from "../utils/checkType"
|
import checkType from "../utils/checkType"
|
||||||
|
import { isArtist, isDuration, isTitle } from "../utils/filters"
|
||||||
import traverse from "../utils/traverse"
|
import traverse from "../utils/traverse"
|
||||||
import traverseList from "../utils/traverseList"
|
import traverseList from "../utils/traverseList"
|
||||||
import traverseString from "../utils/traverseString"
|
import traverseString from "../utils/traverseString"
|
||||||
|
|
@ -28,20 +29,23 @@ export default class VideoParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseSearchResult(item: any): VideoDetailed {
|
public static parseSearchResult(item: any): VideoDetailed {
|
||||||
const flexColumns = traverseList(item, "flexColumns")
|
const columns = traverseList(item, "flexColumns", "runs").flat()
|
||||||
const videoId = traverseString(item, "playNavigationEndpoint", "videoId")()
|
|
||||||
|
const title = columns.find(isTitle)
|
||||||
|
const artist = columns.find(isArtist)
|
||||||
|
const duration = columns.find(isDuration)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "VIDEO",
|
type: "VIDEO",
|
||||||
videoId,
|
videoId: traverseString(item, "playNavigationEndpoint", "videoId")(),
|
||||||
name: traverseString(flexColumns[0], "runs", "text")(),
|
name: traverseString(title, "text")(),
|
||||||
artists: traverseList(flexColumns[1], "runs")
|
artists: [
|
||||||
.filter(run => "navigationEndpoint" in run)
|
{
|
||||||
.map(run => ({
|
name: traverseString(artist, "text")(),
|
||||||
artistId: traverseString(run, "browseId")(),
|
artistId: traverseString(artist, "browseId")(),
|
||||||
name: traverseString(run, "text")(),
|
},
|
||||||
})),
|
],
|
||||||
duration: Parser.parseDuration(traverseString(flexColumns[1], "text")(-1)),
|
duration: Parser.parseDuration(duration.text),
|
||||||
thumbnails: traverseList(item, "thumbnails"),
|
thumbnails: traverseList(item, "thumbnails"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -58,25 +62,28 @@ export default class VideoParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parsePlaylistVideo(item: any): VideoDetailed {
|
public static parsePlaylistVideo(item: any): VideoDetailed {
|
||||||
const flexColumns = traverseList(item, "flexColumns")
|
const columns = traverseList(item, "flexColumns", "runs").flat()
|
||||||
const videoId =
|
|
||||||
traverseString(item, "playNavigationEndpoint", "videoId")() ||
|
const title = columns.find(isTitle) || columns[0]
|
||||||
traverseList(item, "thumbnails")[0].url.match(/https:\/\/i\.ytimg\.com\/vi\/(.+)\//)[1]
|
const artist = columns.find(isArtist) || columns[1]
|
||||||
|
const duration = columns.find(isDuration)
|
||||||
|
|
||||||
return checkType(
|
return checkType(
|
||||||
{
|
{
|
||||||
type: "VIDEO",
|
type: "VIDEO",
|
||||||
videoId,
|
videoId:
|
||||||
name: traverseString(flexColumns[0], "runs", "text")(),
|
traverseString(item, "playNavigationEndpoint", "videoId")() ||
|
||||||
artists: traverseList(flexColumns[1], "runs")
|
traverseList(item, "thumbnails")[0].url.match(
|
||||||
.filter(run => "navigationEndpoint" in run)
|
/https:\/\/i\.ytimg\.com\/vi\/(.+)\//,
|
||||||
.map(run => ({
|
)[1],
|
||||||
artistId: traverseString(run, "browseId")(),
|
name: traverseString(title, "text")(),
|
||||||
name: traverseString(run, "text")(),
|
artists: [
|
||||||
})),
|
{
|
||||||
duration: Parser.parseDuration(
|
name: traverseString(artist, "text")(),
|
||||||
traverseString(item, "fixedColumns", "runs", "text")(),
|
artistId: traverseString(artist, "browseId")() || null,
|
||||||
),
|
},
|
||||||
|
],
|
||||||
|
duration: duration ? Parser.parseDuration(duration.text) : null,
|
||||||
thumbnails: traverseList(item, "thumbnails"),
|
thumbnails: traverseList(item, "thumbnails"),
|
||||||
},
|
},
|
||||||
VideoDetailed,
|
VideoDetailed,
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,14 @@ const errors: Problem[] = []
|
||||||
const queries = ["Lilac", "Weekend", "Eill", "Eminem", "Lisa Hannigan"]
|
const queries = ["Lilac", "Weekend", "Eill", "Eminem", "Lisa Hannigan"]
|
||||||
const expect = (data: any, type: Type) => {
|
const expect = (data: any, type: Type) => {
|
||||||
const result = type(data)
|
const result = type(data)
|
||||||
if (!result.data && result.problems?.length) {
|
if (result.problems?.length) {
|
||||||
errors.push(...result.problems!)
|
errors.push(...result.problems!)
|
||||||
|
} else {
|
||||||
|
const empty = JSON.stringify(result.data).match(/"\w+":""/g)
|
||||||
|
if (empty) {
|
||||||
|
console.log(result.data, empty)
|
||||||
|
}
|
||||||
|
equal(empty, null)
|
||||||
}
|
}
|
||||||
equal(result.problems, undefined)
|
equal(result.problems, undefined)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import traverseString from "./traverseString"
|
||||||
|
|
||||||
|
export const isTitle = (data: any) => {
|
||||||
|
return traverseString(data, "musicVideoType")().startsWith("MUSIC_VIDEO_TYPE_")
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isArtist = (data: any) => {
|
||||||
|
return ["MUSIC_PAGE_TYPE_USER_CHANNEL", "MUSIC_PAGE_TYPE_ARTIST"].includes(
|
||||||
|
traverseString(data, "pageType")(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isAlbum = (data: any) => {
|
||||||
|
return traverseString(data, "pageType")() === "MUSIC_PAGE_TYPE_ALBUM"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isDuration = (data: any) => {
|
||||||
|
return traverseString(data, "text")().match(/(\d{1,2}:)?\d{1,2}:\d{1,2}/)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue