myetv-player 1.7.1 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +461 -155
- package/dist/myetv-player.js +391 -82
- package/dist/myetv-player.min.js +314 -53
- package/package.json +1 -1
- package/plugins/autosub/myetv-autosub-plugin.js +303 -155
- package/plugins/autosub/readme.md +63 -69
- package/plugins/radio/api.php +154 -0
- package/plugins/radio/icons/bolt-lightning-solid.png +0 -0
- package/plugins/radio/icons/compact-disc-solid.png +0 -0
- package/plugins/radio/icons/guitar-solid.png +0 -0
- package/plugins/radio/icons/headphones-solid.png +0 -0
- package/plugins/radio/icons/medal-solid.png +0 -0
- package/plugins/radio/icons/music-solid.png +0 -0
- package/plugins/radio/icons/newspaper-solid.png +0 -0
- package/plugins/radio/icons/radio-solid.png +0 -0
- package/plugins/radio/icons/readme.md +1 -0
- package/plugins/radio/myetv-radio-plugin.js +1655 -0
- package/plugins/radio/php/data/readme.md +1 -0
- package/plugins/radio/php/radio_stats.php +203 -0
- package/plugins/radio/proxy.php +26 -0
- package/plugins/radio/readme.md +176 -0
- package/plugins/radio/station_example.json +47 -0
- package/plugins/video-loading/myetv-video-loading-plugin.js +157 -0
- package/plugins/video-loading/readme.md +87 -0
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
# myetv-autosub-plugin
|
|
2
2
|
|
|
3
|
-
> 🎙️ Auto-subtitles plugin for **myetv-player** — client-side speech recognition via [Whisper AI](https://huggingface.co/Xenova/whisper-tiny) + [Transformers.js](https://github.com/huggingface/transformers.js), no server, no FFmpeg, no API key required.
|
|
3
|
+
> 🎙️ Auto-subtitles plugin for **myetv-player** — client-side speech recognition via [Whisper AI](https://huggingface.co/Xenova/whisper-tiny) + [Transformers.js](https://github.com/huggingface/transformers.js), no server, no FFmpeg, no API key required. Now with **Smart Hybrid Mode** to support native `.vtt` and `.srt` files!
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## ✨ Features
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
9
|
+
- 🎯 **Smart Hybrid Mode** — Automatically detects existing `.vtt` or `.srt` tracks from the player, bypassing AI generation to save bandwidth while taking over custom rendering and translation.
|
|
10
|
+
- 🧠 **100% client-side** — AI transcription runs in a Web Worker, never leaves the browser.
|
|
11
|
+
- ⚡ **Streaming transcription** — subtitles appear chunk by chunk while the model processes audio.
|
|
12
|
+
- 🌍 **Auto-translation** — translate subtitles (both AI-generated and native imported ones) via [MyMemory API](https://mymemory.translated.net/) (free, no key) or self-hosted [LibreTranslate](https://libretranslate.com/) / MarianMT (no limits).
|
|
13
|
+
- 🖱️ **Draggable subtitles** — reposition subtitles anywhere on the video (mouse & touch).
|
|
14
|
+
- 📱 **Responsive text** — font size adapts automatically to screen size via `clamp()`.
|
|
15
|
+
- 💾 **Persistent Cache** — transcriptions **and translations** are cached in `localStorage` and reloaded instantly on the next visit.
|
|
16
|
+
- 🔌 **Zero dependencies** — works with the standard myetv-player plugin system.
|
|
17
|
+
- 🔒 **CSP-safe** — compatible with strict Content Security Policies (`worker-src blob:`).
|
|
17
18
|
|
|
18
19
|
---
|
|
19
20
|
|
|
@@ -21,18 +22,15 @@
|
|
|
21
22
|
|
|
22
23
|
### Option A — Script tag
|
|
23
24
|
```html
|
|
24
|
-
```
|
|
25
|
-
|
|
26
25
|
<script src="myetv-autosub-plugin.js"></script>
|
|
27
26
|
|
|
28
27
|
```
|
|
29
|
-
```
|
|
30
|
-
|
|
31
28
|
|
|
32
29
|
### Option B — Via npm (if bundled with myetv-player)
|
|
33
30
|
|
|
34
31
|
```bash
|
|
35
32
|
npm install myetv-autosub-plugin
|
|
33
|
+
|
|
36
34
|
```
|
|
37
35
|
|
|
38
36
|
> ⚠️ The plugin must be loaded **after** `myetv-player`.
|
|
@@ -42,21 +40,10 @@ npm install myetv-autosub-plugin
|
|
|
42
40
|
## 🚀 Quick Start
|
|
43
41
|
|
|
44
42
|
```html
|
|
45
|
-
<!-- 1. Load the player -->
|
|
46
|
-
```
|
|
47
|
-
|
|
48
43
|
<script src="myetv-player.min.js"></script>
|
|
49
44
|
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
<!-- 2. Load the plugin -->
|
|
53
|
-
```
|
|
54
|
-
|
|
55
45
|
<script src="myetv-autosub-plugin.js"></script>
|
|
56
46
|
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
<!-- 3. Initialize -->
|
|
60
47
|
<script>
|
|
61
48
|
const player = new MYETVvideoplayer('my-video', { /* player options */ });
|
|
62
49
|
|
|
@@ -69,27 +56,26 @@ npm install myetv-autosub-plugin
|
|
|
69
56
|
});
|
|
70
57
|
});
|
|
71
58
|
</script>
|
|
72
|
-
```
|
|
73
59
|
|
|
60
|
+
```
|
|
74
61
|
|
|
75
62
|
---
|
|
76
63
|
|
|
77
64
|
## ⚙️ Options
|
|
78
65
|
|
|
79
66
|
| Option | Type | Default | Description |
|
|
80
|
-
|
|
|
81
|
-
| `language` | `string` | `null` | Source language of the video audio. Accepts ISO codes (`'it'`, `'en'`) or full names (`'italian'`, `'english'`). If `null`, Whisper auto-detects. |
|
|
67
|
+
| --- | --- | --- | --- |
|
|
68
|
+
| `language` | `string` | `null` | Source language of the video audio. Accepts ISO codes (`'it'`, `'en'`) or full names (`'italian'`, `'english'`). If `null`, Whisper auto-detects. *(If native tracks are detected, inherits the track language automatically).* |
|
|
82
69
|
| `modelSize` | `string` | `'base'` | Whisper model size: `'tiny'` (~39 MB), `'base'` (~74 MB), `'small'` (~244 MB). Larger = more accurate but slower. |
|
|
83
|
-
| `autoGenerate` | `boolean` | `false` | Automatically start transcription and show subtitles when the video player is loaded. |
|
|
70
|
+
| `autoGenerate` | `boolean` | `false` | Automatically start transcription and show subtitles when the video player is loaded. *(Ignored if native tracks are detected).* |
|
|
84
71
|
| `autoTranslation` | `string` | `null` | Automatically translate subtitles into this language on load (ISO 2-letter code, e.g. `'en'`, `'it'`, `'fr'`). Requires subtitles to be ready first. |
|
|
85
|
-
| `translationEngine` | `object` | `null` | Translation engine configuration. If `null`, uses MyMemory (free, limited). See [Translation Engines](
|
|
72
|
+
| `translationEngine` | `object` | `null` | Translation engine configuration. If `null`, uses MyMemory (free, limited). See [Translation Engines](https://www.google.com/search?q=%23-translation-engines) below. |
|
|
86
73
|
| `showButton` | `boolean` | `true` | Show the CC button in the player interface. |
|
|
87
74
|
| `position` | `string` | `'topbar'` | Button position: `'right'` or `'left'` (control bar) or `'topbar'` (next to the ⚙️ settings icon). |
|
|
88
75
|
| `subtitleStyle` | `object` | `{}` | Custom inline CSS style object applied to the subtitle text element. |
|
|
89
|
-
| `cacheEnabled` | `boolean` | `true` | Cache transcription in `localStorage` for instant reload. |
|
|
76
|
+
| `cacheEnabled` | `boolean` | `true` | Cache transcription and translations in `localStorage` for instant reload. |
|
|
90
77
|
| `idCache` | `string` | `null` | Stable unique key used for the localStorage cache entry (recommended: your video's database ID). Falls back to a hash of the video URL if not set. |
|
|
91
78
|
|
|
92
|
-
|
|
93
79
|
---
|
|
94
80
|
|
|
95
81
|
## 🌐 Translation Engines
|
|
@@ -99,12 +85,11 @@ The plugin supports three translation backends. By default it uses **MyMemory**
|
|
|
99
85
|
### Comparison
|
|
100
86
|
|
|
101
87
|
| Engine | Limit | Privacy | Setup |
|
|
102
|
-
|
|
|
88
|
+
| --- | --- | --- | --- |
|
|
103
89
|
| **MyMemory** (default) | ⚠️ ~5.000 chars/day per IP | ☁️ External API | None |
|
|
104
90
|
| **LibreTranslate** (self-hosted) | ✅ No limits | 🔒 Your server | Easy |
|
|
105
91
|
| **MarianMT** (self-hosted) | ✅ No limits | 🔒 Your server | Medium |
|
|
106
92
|
|
|
107
|
-
|
|
108
93
|
---
|
|
109
94
|
|
|
110
95
|
### MyMemory (default)
|
|
@@ -116,8 +101,8 @@ Free public API, no configuration required. Limit is approximately **5.000 chara
|
|
|
116
101
|
player.usePlugin('autoSubtitles', {
|
|
117
102
|
autoTranslation: 'it',
|
|
118
103
|
});
|
|
119
|
-
```
|
|
120
104
|
|
|
105
|
+
```
|
|
121
106
|
|
|
122
107
|
---
|
|
123
108
|
|
|
@@ -130,18 +115,22 @@ player.usePlugin('autoSubtitles', {
|
|
|
130
115
|
autoTranslation: 'it',
|
|
131
116
|
translationEngine: {
|
|
132
117
|
type: 'libretranslate',
|
|
133
|
-
url: 'https://your-libretranslate-server.com',
|
|
118
|
+
url: '[https://your-libretranslate-server.com](https://your-libretranslate-server.com)',
|
|
134
119
|
apiKey: 'your-api-key', // optional, required if --api-keys is enabled
|
|
135
120
|
},
|
|
136
121
|
});
|
|
122
|
+
|
|
137
123
|
```
|
|
138
124
|
|
|
139
125
|
**Self-hosting with Docker:**
|
|
140
126
|
|
|
141
127
|
```bash
|
|
142
128
|
docker run -p 5000:5000 libretranslate/libretranslate --disable-web-ui --api-keys
|
|
129
|
+
|
|
143
130
|
```
|
|
131
|
+
|
|
144
132
|
LibreTranslate: [https://github.com/LibreTranslate/LibreTranslate](https://github.com/LibreTranslate/LibreTranslate)
|
|
133
|
+
|
|
145
134
|
---
|
|
146
135
|
|
|
147
136
|
### MarianMT
|
|
@@ -153,9 +142,10 @@ player.usePlugin('autoSubtitles', {
|
|
|
153
142
|
autoTranslation: 'it',
|
|
154
143
|
translationEngine: {
|
|
155
144
|
type: 'marianmt',
|
|
156
|
-
url: 'https://your-marianmt-server.com',
|
|
145
|
+
url: '[https://your-marianmt-server.com](https://your-marianmt-server.com)',
|
|
157
146
|
},
|
|
158
147
|
});
|
|
148
|
+
|
|
159
149
|
```
|
|
160
150
|
|
|
161
151
|
Expected request/response format:
|
|
@@ -167,8 +157,8 @@ Expected request/response format:
|
|
|
167
157
|
|
|
168
158
|
// Response
|
|
169
159
|
{ "translatedText": "Ciao mondo" }
|
|
170
|
-
```
|
|
171
160
|
|
|
161
|
+
```
|
|
172
162
|
|
|
173
163
|
---
|
|
174
164
|
|
|
@@ -178,27 +168,27 @@ Clicking the **CC button** opens a context menu with three sections:
|
|
|
178
168
|
|
|
179
169
|
### Subtitles
|
|
180
170
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
171
|
+
* **▶ Enable** — show subtitles
|
|
172
|
+
* **✕ Disable** — hide subtitles
|
|
184
173
|
|
|
185
174
|
### Management
|
|
186
175
|
|
|
187
|
-
|
|
188
|
-
- **🔄 Regenerate transcription** — force re-transcription (discards cache)
|
|
189
|
-
- **📊 Show progress** — open the transcription progress panel (visible while generating)
|
|
176
|
+
*(Note: This section is automatically hidden if native subtitles `.vtt`/`.srt` are detected and imported from the player).*
|
|
190
177
|
|
|
178
|
+
* **🎙️ Generate subtitles** — start transcription (first time)
|
|
179
|
+
* **🔄 Regenerate transcription** — force re-transcription (discards cache)
|
|
180
|
+
* **📊 Show progress** — open the transcription progress panel (visible while generating)
|
|
191
181
|
|
|
192
182
|
### Translate to
|
|
193
183
|
|
|
194
|
-
Scrollable list of 17+ languages. Translations are cached in
|
|
184
|
+
Scrollable list of 17+ languages. Translations are cached in `localStorage` for the session — switching back to a previously translated language is completely instant and requires no API calls.
|
|
195
185
|
|
|
196
186
|
---
|
|
197
187
|
|
|
198
188
|
## 🌍 Supported Translation Languages
|
|
199
189
|
|
|
200
190
|
| Code | Language |
|
|
201
|
-
|
|
|
191
|
+
| --- | --- |
|
|
202
192
|
| `off` | No translation (original) |
|
|
203
193
|
| `it` | 🇮🇹 Italiano |
|
|
204
194
|
| `en` | 🇬🇧 English |
|
|
@@ -217,13 +207,12 @@ Scrollable list of 17+ languages. Translations are cached in memory for the sess
|
|
|
217
207
|
| `uk` | 🇺🇦 Українська |
|
|
218
208
|
| `hi` | 🇮🇳 हिन्दी |
|
|
219
209
|
|
|
220
|
-
|
|
221
210
|
---
|
|
222
211
|
|
|
223
212
|
## 🧠 Whisper Models
|
|
224
213
|
|
|
225
214
|
| Size | ID | Download | Speed | Accuracy |
|
|
226
|
-
|
|
|
215
|
+
| --- | --- | --- | --- | --- |
|
|
227
216
|
| `tiny` | `Xenova/whisper-tiny` | ~39 MB | ⚡⚡⚡ Fast | ★☆☆ Good |
|
|
228
217
|
| `base` | `Xenova/whisper-base` | ~74 MB | ⚡⚡ Medium | ★★☆ Better |
|
|
229
218
|
| `small` | `Xenova/whisper-small` | ~244 MB | ⚡ Slow | ★★★ Best |
|
|
@@ -239,11 +228,12 @@ If your site uses a Content Security Policy, add the following:
|
|
|
239
228
|
```http
|
|
240
229
|
Content-Security-Policy:
|
|
241
230
|
worker-src blob:;
|
|
242
|
-
script-src blob: https://cdn.jsdelivr.net;
|
|
243
|
-
connect-src https://cdn.jsdelivr.net
|
|
244
|
-
https://huggingface.co
|
|
245
|
-
https://api.mymemory.translated.net
|
|
246
|
-
https://your-libretranslate-server.com;
|
|
231
|
+
script-src blob: [https://cdn.jsdelivr.net](https://cdn.jsdelivr.net);
|
|
232
|
+
connect-src [https://cdn.jsdelivr.net](https://cdn.jsdelivr.net)
|
|
233
|
+
[https://huggingface.co](https://huggingface.co)
|
|
234
|
+
[https://api.mymemory.translated.net](https://api.mymemory.translated.net)
|
|
235
|
+
[https://your-libretranslate-server.com](https://your-libretranslate-server.com);
|
|
236
|
+
|
|
247
237
|
```
|
|
248
238
|
|
|
249
239
|
> If using LibreTranslate or MarianMT, replace `https://api.mymemory.translated.net` with your server URL (or add both if using MyMemory as fallback).
|
|
@@ -254,12 +244,12 @@ Use `crossorigin="anonymous"` on the `<video>` element.
|
|
|
254
244
|
|
|
255
245
|
## 💾 Cache
|
|
256
246
|
|
|
257
|
-
Transcriptions are stored in `localStorage` under the key `myetv_autosub_<id>`.
|
|
247
|
+
Transcriptions and Translations are stored in `localStorage` under the key `myetv_autosub_<id>`.
|
|
258
248
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
249
|
+
* Up to **30 entries** are kept — oldest are evicted automatically
|
|
250
|
+
* Set `idCache: 'your-video-id'` for a stable, human-readable key
|
|
251
|
+
* Set `cacheEnabled: false` to disable caching entirely
|
|
252
|
+
* Use **"🔄 Regenerate transcription"** from the menu to force a fresh AI transcription
|
|
263
253
|
|
|
264
254
|
---
|
|
265
255
|
|
|
@@ -267,8 +257,8 @@ Transcriptions are stored in `localStorage` under the key `myetv_autosub_<id>`.
|
|
|
267
257
|
|
|
268
258
|
Hover over the subtitle text to reveal the **⠿ sposta** drag handle. Click and drag to reposition subtitles anywhere inside the video frame.
|
|
269
259
|
|
|
270
|
-
|
|
271
|
-
|
|
260
|
+
* Works with mouse and touch (touch drag handle is hidden on mobile — drag directly on the subtitle text)
|
|
261
|
+
* Position resets on page reload (not persisted)
|
|
272
262
|
|
|
273
263
|
---
|
|
274
264
|
|
|
@@ -280,13 +270,13 @@ player.on('playerready', () => {
|
|
|
280
270
|
language: 'english', // language of the audio: "english" or "en" (if empty auto-detect)
|
|
281
271
|
modelSize: 'base', // AI model: 'tiny', 'base', 'small'
|
|
282
272
|
autoGenerate: true, // start transcription automatically
|
|
283
|
-
autoTranslation: 'it',
|
|
273
|
+
autoTranslation: 'it', // auto-translate to Italian when ready
|
|
284
274
|
position: 'topbar', // CC button position: 'left', 'right', 'topbar'
|
|
285
275
|
cacheEnabled: true,
|
|
286
276
|
idCache: `video-${videoId}`,
|
|
287
277
|
translationEngine: {
|
|
288
278
|
type: 'libretranslate',
|
|
289
|
-
url: 'https://your-libretranslate-server.com',
|
|
279
|
+
url: '[https://your-libretranslate-server.com](https://your-libretranslate-server.com)',
|
|
290
280
|
apiKey: 'your-api-key',
|
|
291
281
|
},
|
|
292
282
|
subtitleStyle: {
|
|
@@ -295,22 +285,29 @@ player.on('playerready', () => {
|
|
|
295
285
|
},
|
|
296
286
|
});
|
|
297
287
|
});
|
|
298
|
-
```
|
|
299
288
|
|
|
289
|
+
```
|
|
300
290
|
|
|
301
291
|
---
|
|
302
292
|
|
|
303
293
|
## 🏗️ How It Works
|
|
304
294
|
|
|
305
295
|
```
|
|
306
|
-
User clicks CC
|
|
296
|
+
User clicks CC (or Auto-Trigger starts)
|
|
297
|
+
│
|
|
298
|
+
▼
|
|
299
|
+
Check for Native Player Subtitles (.vtt / .srt)
|
|
300
|
+
│
|
|
301
|
+
Yes ──────────────────────► Import seamlessly, take over UI rendering, skip AI.
|
|
302
|
+
│
|
|
303
|
+
No
|
|
307
304
|
│
|
|
308
305
|
▼
|
|
309
306
|
Check localStorage cache
|
|
310
307
|
│
|
|
311
|
-
|
|
308
|
+
Hit ──────────────────────► Load transcriptions instantly
|
|
312
309
|
│
|
|
313
|
-
|
|
310
|
+
Miss
|
|
314
311
|
│
|
|
315
312
|
▼
|
|
316
313
|
Extract audio (fetch or captureStream)
|
|
@@ -333,8 +330,8 @@ Save to localStorage cache
|
|
|
333
330
|
▼
|
|
334
331
|
Translate via selected engine
|
|
335
332
|
(MyMemory / LibreTranslate / MarianMT)
|
|
336
|
-
```
|
|
337
333
|
|
|
334
|
+
```
|
|
338
335
|
|
|
339
336
|
---
|
|
340
337
|
|
|
@@ -351,6 +348,3 @@ Pull requests welcome. For major changes, please open an issue first.
|
|
|
351
348
|
---
|
|
352
349
|
|
|
353
350
|
> Built with ❤️ from the [MYETV](https://www.myetv.tv) team.
|
|
354
|
-
|
|
355
|
-
```
|
|
356
|
-
```
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
ini_set('memory_limit', '512M');
|
|
3
|
+
ini_set('max_execution_time', '60');
|
|
4
|
+
//error_reporting(0);
|
|
5
|
+
//ini_set('display_errors', 0);
|
|
6
|
+
header('Content-Type: application/json');
|
|
7
|
+
|
|
8
|
+
$stationsFile = __DIR__ . '/stations_example.json';
|
|
9
|
+
|
|
10
|
+
// Check 1: file exists and is readable
|
|
11
|
+
if (!file_exists($stationsFile) || !is_readable($stationsFile)) {
|
|
12
|
+
http_response_code(503);
|
|
13
|
+
echo json_encode(['error' => 'stations.json not found', 'path' => $stationsFile]);
|
|
14
|
+
exit;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
$raw = file_get_contents($stationsFile);
|
|
18
|
+
|
|
19
|
+
// Check 2: file was read correctly
|
|
20
|
+
if ($raw === false || empty($raw)) {
|
|
21
|
+
http_response_code(503);
|
|
22
|
+
echo json_encode(['error' => 'stations.json is empty or unreadable']);
|
|
23
|
+
exit;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
$stations = json_decode($raw, true);
|
|
27
|
+
|
|
28
|
+
// Check 3: JSON is valid
|
|
29
|
+
if (!is_array($stations)) {
|
|
30
|
+
http_response_code(503);
|
|
31
|
+
echo json_encode(['error' => 'stations.json invalid JSON', 'json_error' => json_last_error_msg()]);
|
|
32
|
+
exit;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Common filters (used by all endpoints)
|
|
36
|
+
$countryFilter = isset($_GET['country']) ? strtolower(trim($_GET['country'])) : '';
|
|
37
|
+
$languageFilter = isset($_GET['language']) ? strtolower(trim($_GET['language'])) : '';
|
|
38
|
+
|
|
39
|
+
// All light - lightweight station list (DEFAULT if no other params)
|
|
40
|
+
if (isset($_GET['channel']) && $_GET['channel'] === 'all_light') {
|
|
41
|
+
$offset = (int)($_GET['offset'] ?? 0);
|
|
42
|
+
$limit = (int)($_GET['limit'] ?? 50000);
|
|
43
|
+
|
|
44
|
+
$light = array_map(fn($s) => [
|
|
45
|
+
'channel_id' => $s['channel_id'] ?? 0,
|
|
46
|
+
'name' => $s['name'] ?? '',
|
|
47
|
+
'url_resolved' => $s['url_resolved'] ?? ($s['url'] ?? ''),
|
|
48
|
+
'countrycode' => $s['countrycode'] ?? '',
|
|
49
|
+
'language' => $s['language'] ?? '',
|
|
50
|
+
'tags' => $s['tags'] ?? '',
|
|
51
|
+
'favicon' => $s['favicon'] ?? '',
|
|
52
|
+
], $stations);
|
|
53
|
+
|
|
54
|
+
// Remove stations without URL
|
|
55
|
+
$light = array_values(array_filter(
|
|
56
|
+
$light,
|
|
57
|
+
fn($s) => !empty($s['url_resolved'])
|
|
58
|
+
));
|
|
59
|
+
|
|
60
|
+
// Country filter (exact match on countrycode)
|
|
61
|
+
if ($countryFilter) {
|
|
62
|
+
$light = array_values(array_filter(
|
|
63
|
+
$light,
|
|
64
|
+
fn($s) => strtolower($s['countrycode'] ?? '') === $countryFilter
|
|
65
|
+
));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Language filter (substring match in language OR tags)
|
|
69
|
+
if ($languageFilter) {
|
|
70
|
+
$light = array_values(array_filter(
|
|
71
|
+
$light,
|
|
72
|
+
fn($s) =>
|
|
73
|
+
str_contains(strtolower($s['language'] ?? ''), $languageFilter) ||
|
|
74
|
+
str_contains(strtolower($s['tags'] ?? ''), $languageFilter)
|
|
75
|
+
));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
echo json_encode(array_values(array_slice($light, $offset, $limit)));
|
|
79
|
+
exit;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Single channel by number
|
|
83
|
+
if (isset($_GET['channel']) && is_numeric($_GET['channel'])) {
|
|
84
|
+
$ch = (int)$_GET['channel'];
|
|
85
|
+
$result = array_values(array_filter($stations, fn($s) => ($s['channel_id'] ?? 0) === $ch));
|
|
86
|
+
echo json_encode($result[0] ?? null);
|
|
87
|
+
exit;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Search by name
|
|
91
|
+
if (isset($_GET['search'])) {
|
|
92
|
+
$q = strtolower(trim($_GET['search']));
|
|
93
|
+
|
|
94
|
+
$result = array_values(array_filter($stations,
|
|
95
|
+
fn($s) => str_contains(strtolower($s['name'] ?? ''), $q)
|
|
96
|
+
));
|
|
97
|
+
|
|
98
|
+
// Apply country/language filters to search results too
|
|
99
|
+
if ($countryFilter) {
|
|
100
|
+
$result = array_values(array_filter(
|
|
101
|
+
$result,
|
|
102
|
+
fn($s) => strtolower($s['countrycode'] ?? '') === $countryFilter
|
|
103
|
+
));
|
|
104
|
+
}
|
|
105
|
+
if ($languageFilter) {
|
|
106
|
+
$result = array_values(array_filter(
|
|
107
|
+
$result,
|
|
108
|
+
fn($s) =>
|
|
109
|
+
str_contains(strtolower($s['language'] ?? ''), $languageFilter) ||
|
|
110
|
+
str_contains(strtolower($s['tags'] ?? ''), $languageFilter)
|
|
111
|
+
));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
echo json_encode(array_slice($result, 0, 20));
|
|
115
|
+
exit;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// NO PARAMETERS = return entire list (lightweight)
|
|
119
|
+
$offset = (int)($_GET['offset'] ?? 0);
|
|
120
|
+
$limit = (int)($_GET['limit'] ?? 50000);
|
|
121
|
+
|
|
122
|
+
$light = array_map(fn($s) => [
|
|
123
|
+
'channel_id' => $s['channel_id'] ?? 0,
|
|
124
|
+
'name' => $s['name'] ?? '',
|
|
125
|
+
'url_resolved' => $s['url_resolved'] ?? ($s['url'] ?? ''),
|
|
126
|
+
'countrycode' => $s['countrycode'] ?? '',
|
|
127
|
+
'language' => $s['language'] ?? '',
|
|
128
|
+
'tags' => $s['tags'] ?? '',
|
|
129
|
+
'favicon' => $s['favicon'] ?? '',
|
|
130
|
+
], $stations);
|
|
131
|
+
|
|
132
|
+
// Remove stations without URL
|
|
133
|
+
$light = array_values(array_filter(
|
|
134
|
+
$light,
|
|
135
|
+
fn($s) => !empty($s['url_resolved'])
|
|
136
|
+
));
|
|
137
|
+
|
|
138
|
+
// Apply filters if specified
|
|
139
|
+
if ($countryFilter) {
|
|
140
|
+
$light = array_values(array_filter(
|
|
141
|
+
$light,
|
|
142
|
+
fn($s) => strtolower($s['countrycode'] ?? '') === $countryFilter
|
|
143
|
+
));
|
|
144
|
+
}
|
|
145
|
+
if ($languageFilter) {
|
|
146
|
+
$light = array_values(array_filter(
|
|
147
|
+
$light,
|
|
148
|
+
fn($s) =>
|
|
149
|
+
str_contains(strtolower($s['language'] ?? ''), $languageFilter) ||
|
|
150
|
+
str_contains(strtolower($s['tags'] ?? ''), $languageFilter)
|
|
151
|
+
));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
echo json_encode(array_values(array_slice($light, $offset, $limit)));
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|