A music player that connects to your cloud/distributed storage.
1//
2// Search worker
3// (◡ ‿ ◡ ✿)
4//
5// This worker is responsible for searching through a `Track` collection.
6
7
8import lunr from "lunr"
9
10
11const FIELDS = ["album", "artist", "title"]
12
13
14lunr.Pipeline.registerFunction(
15 removeParenthesesFromToken,
16 "Remove parentheses from token"
17)
18
19
20let index: lunr.Index
21
22
23
24// Incoming messages
25// -----------------
26
27self.onmessage = (event: MessageEvent) => {
28 switch (event.data.action) {
29 case "PERFORM_SEARCH":
30 performSearch(event.data.data)
31 break
32
33 case "UPDATE_SEARCH_INDEX":
34 updateSearchIndex(event.data.data)
35 break
36 }
37}
38
39
40
41// Mapper
42// ------
43
44const mapTrack = track => ({
45 id: track.id,
46 album: track.tags.album,
47 artist: track.tags.artist,
48 title: track.tags.title,
49})
50
51
52
53// Actions
54// -------
55
56function performSearch(rawSearchTerm: string) {
57 let results: string[] =
58 []
59
60 const searchTerm = rawSearchTerm
61 .replace(/-\s+/g, "-")
62 .replace(/\+\s+/g, "+")
63 .split(/ +/)
64 .reduce(
65 ([ acc, previousOperator, previousPrefix ]: [ string[], string, string ], chunk: string): [ string[], string, string ] => {
66 const operator = (a => a && a[0])( chunk.match(/^(\+|-)/) )
67
68 let chunkWithoutOperator = chunk.replace(/^(\+|-)/, "").replace(/\*$/, "").trim()
69 let prefix = (a => a && a[1])( chunkWithoutOperator.match(/^([^:]+:)/) )
70 let chunkWithoutPrefix = chunkWithoutOperator.replace(/^([^:]+:)/, "")
71
72 if (prefix && !FIELDS.includes(prefix.slice(0, -1))) {
73 prefix = null
74 chunkWithoutPrefix = chunkWithoutOperator.replace(":", "\\:")
75 chunkWithoutOperator = chunkWithoutPrefix
76
77 } else if (prefix && chunkWithoutPrefix.includes(":")) {
78 chunkWithoutPrefix = chunkWithoutPrefix.replace(":", "\\:")
79 chunkWithoutOperator = prefix + chunkWithoutPrefix
80
81 }
82
83 const op = operator || previousOperator
84 const pr = prefix ? "" : (operator ? "" : previousPrefix)
85
86 return chunkWithoutPrefix.trim().length > 0
87 ? [ [ ...acc
88 , op + pr + chunkWithoutOperator
89 ]
90 , op
91 , prefix || pr
92 ]
93 : [ acc, previousOperator, previousPrefix ]
94 },
95 [ [], "+", "" ]
96 )[0]
97 .join(" ")
98
99 const searchTermWithAsteriks =
100 searchTerm
101 .split(" ")
102 .map(s => {
103 if (s.startsWith("-")) return s
104 return s + "*"
105 })
106 .join(" ")
107
108 if (index) {
109 results = index
110 .search(searchTerm)
111 .map(s => s.ref)
112 .concat(
113 index
114 .search(searchTermWithAsteriks)
115 .map(s => s.ref)
116 )
117 }
118
119 self.postMessage({
120 action: "PERFORM_SEARCH",
121 data: results
122 })
123}
124
125
126function updateSearchIndex(input: string | object[]) {
127 const tracks = (typeof input == "string")
128 ? JSON.parse(input)
129 : input
130
131 index = customLunr((builder: lunr.Builder) => {
132 FIELDS.forEach(
133 field => builder.field(field)
134 )
135
136 ;(tracks || [])
137 .map(mapTrack)
138 .forEach(t => builder.add(t))
139 })
140}
141
142
143
144function customLunr(fn: (b: lunr.Builder) => void) {
145 const builder = new lunr.Builder
146
147 builder.pipeline.add(removeParenthesesFromToken, lunr.stemmer)
148 builder.searchPipeline.add(removeParenthesesFromToken, lunr.stemmer)
149
150 fn(builder)
151 return builder.build()
152}
153
154
155function removeParenthesesFromToken(token: lunr.Token): lunr.Token {
156 return token.update(s => s.replace(/\(|\)/, ""))
157}