├── .gitignore
├── .prettierrc
├── APIDESIGN.md
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── bin.js
├── collections.js
├── crud.js
├── filebrowser.js
├── fileformats.js
├── install.ps1
├── install.sh
├── mpvremote.conf
├── mpvremote
└── main.js
├── package.json
├── postman
├── MPV Remote.postman_collection.json
└── MPV Remote.postman_environment.json
├── remoteServer.js
├── settings.js
└── watchlisthandler.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | npm-debug.log
4 | .env
5 | .DS_Store
6 | *.tgz
7 | .prettierrc
8 | package-lock.json
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false
4 | }
5 |
--------------------------------------------------------------------------------
/APIDESIGN.md:
--------------------------------------------------------------------------------
1 | # Postman
2 |
3 | [You can download Postman collection and environment here](https://github.com/husudosu/mpv-remote-node/tree/master/postman)
4 |
5 | For safety reasons computer actions route not included!
6 |
7 | # Responses
8 |
9 | Default response:
10 |
11 | ```json
12 | { "message": "success or error message" }
13 | ```
14 |
15 | # API Routes
16 |
17 | - MPV Status
18 | - Playback controls
19 | - Playlist
20 | - Tracks
21 | - Filebrowser
22 | - Collection handling
23 | - Computer actions
24 |
25 | # Status
26 |
27 | ## /api/v1/mpvinfo
28 |
29 | **Methods:** GET
30 |
31 | Gets info about MPV. MPV remote plugin settings also included.
32 |
33 | ## /api/v1/status
34 |
35 | **Methods:** GET
36 |
37 | Gets status of the player.
38 |
39 | **Query parameters:**
40 |
41 | **exclude:** You can provide properties to exclude as list.
42 |
43 | - example value: exclude=playlist,track-list
44 |
45 | Example response:
46 |
47 | ```json
48 | {
49 | "pause": true,
50 | "mute": false,
51 | "filename": "Dorohedoro 02 - In the Bag - Eat Quietly During Meals - My Neighbor the Sorcerer.mkv",
52 | "duration": 1422.061,
53 | "position": 71.787,
54 | "remaining": 1350.274,
55 | "media-title": "Dorohedoro 02 - In the Bag | Eat Quietly During Meals | My Neighbor the Sorcerer",
56 | "playlist": [
57 | {
58 | "index": 0,
59 | "id": 1,
60 | "filePath": "V:\\anime\\Vivy - Fluorite Eye's Song S01 1080p\\Vivy - Fluorite Eye's Song - E02.mkv",
61 | "filename": "Vivy - Fluorite Eye's Song - E02.mkv"
62 | },
63 | {
64 | "index": 1,
65 | "id": 2,
66 | "filePath": "V:\\anime\\Dorohedoro S01+OVA (BD 1080p)\\Dorohedoro 02 - In the Bag - Eat Quietly During Meals - My Neighbor the Sorcerer.mkv",
67 | "current": true,
68 | "filename": "Dorohedoro 02 - In the Bag - Eat Quietly During Meals - My Neighbor the Sorcerer.mkv"
69 | }
70 | ],
71 | "chapter": 0,
72 | "chapter-list": [
73 | {
74 | "title": "Intro",
75 | "time": 0
76 | },
77 | {
78 | "title": "OP",
79 | "time": 68.986
80 | },
81 | {
82 | "title": "Part A",
83 | "time": 159.034
84 | },
85 | {
86 | "title": "Part B",
87 | "time": 589.089
88 | },
89 | {
90 | "title": "ED",
91 | "time": 1316.023
92 | },
93 | {
94 | "title": "Preview",
95 | "time": 1406.03
96 | }
97 | ],
98 | "volume": 100,
99 | "max-volume": 100,
100 | "fullscreen": false,
101 | "speed": 1,
102 | "sub-delay": 0,
103 | "sub-visibility": true,
104 | "track-list": [
105 | {
106 | "index": 0,
107 | "id": 1,
108 | "type": "video",
109 | "selected": true,
110 | "codec": "hevc",
111 | "demux-w": 1920,
112 | "demux-h": 1080
113 | },
114 | {
115 | "index": 1,
116 | "id": 1,
117 | "type": "audio",
118 | "selected": true,
119 | "codec": "opus",
120 | "demux-channel-count": 2,
121 | "demux-channels": "unknown2",
122 | "demux-samplerate": 48000,
123 | "lang": "jpn"
124 | },
125 | {
126 | "index": 2,
127 | "id": 2,
128 | "type": "audio",
129 | "selected": false,
130 | "codec": "opus",
131 | "demux-channel-count": 2,
132 | "demux-channels": "unknown2",
133 | "demux-samplerate": 48000,
134 | "lang": "eng"
135 | },
136 | {
137 | "index": 3,
138 | "id": 1,
139 | "type": "sub",
140 | "selected": true,
141 | "codec": "subrip",
142 | "lang": "hun"
143 | },
144 | {
145 | "index": 4,
146 | "id": 2,
147 | "type": "sub",
148 | "selected": false,
149 | "codec": "ass",
150 | "lang": "eng"
151 | },
152 | {
153 | "index": 5,
154 | "id": 3,
155 | "type": "sub",
156 | "selected": false,
157 | "codec": "ass",
158 | "lang": "eng"
159 | }
160 | ],
161 | "audio-delay": 0,
162 | "sub-font-size": 55,
163 | "sub-ass-override": true,
164 | "metadata": {}
165 | }
166 | ```
167 |
168 | # Media controls
169 |
170 | ## /api/v1/controls/play-pause
171 |
172 | **Methods:** POST
173 |
174 | Cycles between play and pause
175 |
176 | ## /api/v1/controls/play
177 |
178 | **Methods:** POST
179 |
180 | Start playback
181 |
182 | ## /api/v1/controls/pause
183 |
184 | **Methods:** POST
185 |
186 | Pause playback
187 |
188 | ## /api/v1/controls/stop
189 |
190 | **Methods:** POST
191 |
192 | Stops the playback, also clears playlist
193 |
194 | ## /api/v1/controls/prev
195 |
196 | Alias for /playlist/prev
197 |
198 | ## /api/v1/controls/next
199 |
200 | Alias for /playlist/next
201 |
202 | ## /api/v1/controls/fullscreen
203 |
204 | **Methods:** POST
205 |
206 | Toggles fullscreen mode
207 |
208 | ## /api/v1/controls/mute
209 |
210 | **Methods:** POST
211 |
212 | Mutes volume
213 |
214 | ## /api/v1/controls/volume/:value
215 |
216 | **Methods:** POST
217 |
218 | Sets volume
219 |
220 | ## /api/v1/controls/seek
221 |
222 | **Methods:** POST
223 |
224 | Seek
225 |
226 | Request JSON:
227 |
228 | ```json
229 | {
230 | "target": 10.0,
231 | "flag": "absolute-percent" // if no flag provided defaults to relative
232 | }
233 | ```
234 |
235 | ### Flags
236 |
237 | - relative (Default)
238 | - absolute
239 |
240 | # Playlist
241 |
242 | ## /api/v1/playlist
243 |
244 | **Methods:** GET, POST
245 |
246 | **Related MPV documentation:** https://mpv.io/manual/master/#command-interface-[%3Coptions%3E]
247 |
248 | ### **GET**
249 |
250 | Gets Playlist items.
251 |
252 | Response JSON:
253 |
254 | ```json
255 | [
256 | {
257 | "index": 0,
258 | "id": 1,
259 | "filePath": "V:\\anime\\Vivy - Fluorite Eye's Song S01 1080p\\Vivy - Fluorite Eye's Song - E02.mkv",
260 | "current": true,
261 | "filename": "Vivy - Fluorite Eye's Song - E02.mkv"
262 | },
263 | {
264 | "index": 1,
265 | "id": 2,
266 | "filePath": "V:\\anime\\Dorohedoro S01+OVA (BD 1080p)\\Dorohedoro 02 - In the Bag - Eat Quietly During Meals - My Neighbor the Sorcerer.mkv",
267 | "filename": "Dorohedoro 02 - In the Bag - Eat Quietly During Meals - My Neighbor the Sorcerer.mkv"
268 | }
269 | ]
270 | ```
271 |
272 | ### **POST**
273 |
274 | Puts an item to playlist.
275 |
276 | **Request JSON:**
277 |
278 | ```JSON
279 | {
280 | "filename": "V:\\anime\\Vivy - Fluorite Eye's Song S01 1080p\\Vivy - Fluorite Eye's Song - E02.mkv",
281 | "flag": "replace",
282 | "seekTo": 60.0, // seekTo works only if flag is replace. Format seconds
283 | "file-local-options": [
284 | "http-header-fields": "'MyHeader: 1234', 'MyHeader2: 5678'"
285 | ]
286 | }
287 | ```
288 |
289 | **flag** possible values:
290 |
291 | - replace
292 | - append
293 | - append-play (Default)
294 |
295 | ## /api/v1/playlist/remove/:index
296 |
297 | **Methods:** DELETE
298 |
299 | Deletes a playlist item.
300 |
301 | ## /api/v1/playlist/move?fromIndex=0&toIndex=1
302 |
303 | **Methods**: POST
304 |
305 | **Related MPV documentation:** https://mpv.io/manual/master/#command-interface-playlist-move
306 |
307 | **Query parameters**:
308 |
309 | - **fromIndex (REQUIRED):** Moves this item.
310 | - **toIndex (REQUIRED):** To this index.
311 |
312 | Moves a playlist item (fromIndex), to desired destination (toIndex).
313 |
314 | ## /api/v1/playlist/play/:index
315 |
316 | **Methods**: POST
317 |
318 | Plays playlist item.
319 |
320 | Note: index can be current too, whcih gonna reload the current entry.
321 |
322 | ## /api/v1/playlist/prev
323 |
324 | **Methods**: POST
325 |
326 | Playlist previous item on playlist
327 |
328 | ## /api/v1/playlist/next
329 |
330 | **Methods**: POST
331 |
332 | Playlist next item on playlist
333 |
334 | ## /api/v1/playlist/clear
335 |
336 | **Methods**: POST
337 |
338 | Clears playlist.
339 |
340 | ## /api/v1/playlist/shuffle
341 |
342 | **Methods**: POST
343 |
344 | Shuffle the playlist.
345 |
346 | # Tracks
347 |
348 | ## /api/v1/tracks
349 |
350 | **Methods:** GET
351 |
352 | Gets all audio, video, subtitle tracks.
353 |
354 | Example response:
355 |
356 | ```json
357 | [
358 | {
359 | "index": 0,
360 | "id": 1,
361 | "type": "video",
362 | "selected": true,
363 | "codec": "hevc",
364 | "demux-w": 1920,
365 | "demux-h": 1080
366 | },
367 | {
368 | "index": 1,
369 | "id": 1,
370 | "type": "audio",
371 | "selected": true,
372 | "codec": "opus",
373 | "demux-channel-count": 2,
374 | "demux-channels": "unknown2",
375 | "demux-samplerate": 48000,
376 | "lang": "jpn"
377 | },
378 | {
379 | "index": 2,
380 | "id": 2,
381 | "type": "audio",
382 | "selected": false,
383 | "codec": "opus",
384 | "demux-channel-count": 2,
385 | "demux-channels": "unknown2",
386 | "demux-samplerate": 48000,
387 | "lang": "eng"
388 | },
389 | {
390 | "index": 3,
391 | "id": 1,
392 | "type": "sub",
393 | "selected": true,
394 | "codec": "subrip",
395 | "lang": "hun"
396 | },
397 | {
398 | "index": 4,
399 | "id": 2,
400 | "type": "sub",
401 | "selected": false,
402 | "codec": "ass",
403 | "lang": "eng"
404 | },
405 | {
406 | "index": 5,
407 | "id": 3,
408 | "type": "sub",
409 | "selected": false,
410 | "codec": "ass",
411 | "lang": "eng"
412 | }
413 | ]
414 | ```
415 |
416 | ## /api/v1/tracks/audio/reload/:id
417 |
418 | **Methods:** POST
419 |
420 | Loads desired audio track ID
421 |
422 | ## /api/v1/tracks/audio/cycle
423 |
424 | **Methods:** POST
425 |
426 | Cycles through audio tracks
427 |
428 | ## /api/v1/tracks/audio/add
429 |
430 | **Methods:** POST
431 |
432 | Adds an audio track
433 |
434 | **Request JSON:**
435 |
436 | ```json
437 | {
438 | "filename": "/home/usr/myaudio.mp3",
439 | "flag": "select" // if no flag provided defaults to select
440 | }
441 | ```
442 |
443 | ### Flags
444 |
445 | - select
446 | - auto
447 | - cached
448 |
449 | ## /api/v1/tracks/audio/timing/:seconds
450 |
451 | **Methods:** POST
452 |
453 | Sets audio delay to provided second, can be negative.
454 |
455 | ## /api/v1/tracks/sub/reload/:id
456 |
457 | **Methods:** POST
458 |
459 | Loads desired subtitle track ID
460 |
461 | ## /api/v1/tracks/sub/timing/:seconds
462 |
463 | **Methods:** POST
464 |
465 | Sets sub delay to provided value, can be negative number.
466 |
467 | ## /api/v1/tracks/sub/ass-override/:value
468 |
469 | **Methods:** POST
470 |
471 | Changes default behaviour of rendering ASS/SSA subtitles.
472 | MPV mostly renders ASS/SSA subtitles correctly, but if you need it, use it.
473 |
474 | **MPV related documentation:** https://mpv.io/manual/master/#options-sub-ass-override
475 |
476 | **BE VERY CAREFUL SOME VALUES CAN BREAK PLAYBACK CHECK DOCUMENTATION ABOVE**
477 |
478 | Possible values:
479 |
480 | - no
481 | - yes
482 | - force
483 | - scale
484 | - strip
485 |
486 | ## /api/v1/tracks/sub/toggle-visibility
487 |
488 | **Methods:** POST
489 |
490 | Toggles subtitle visibility
491 |
492 | ## /api/v1/tracks/sub/visibility/:value
493 |
494 | **Methods:** POST
495 | Sets subtitle visibility
496 |
497 | Values can be:
498 |
499 | - true
500 | - false
501 |
502 | ## /api/v1/tracks/sub/add
503 |
504 | **Methods:** POST
505 |
506 | Add subtitle file.
507 |
508 | **Request JSON:**
509 |
510 | ```json
511 | {
512 | "filename": "/home/usr/mysub.srt",
513 | "flag": "select" // if no flag provided defaults to select
514 | }
515 | ```
516 |
517 | ### Flags
518 |
519 | - select
520 | - auto
521 | - cached
522 |
523 | # Filebrowser
524 |
525 | Basic filebrowser which only accepts paths which included at server configured variable `mpvremote-filebrowserpaths`
526 |
527 | ## /api/v1/filebrowser/paths
528 |
529 | **Methods**: GET
530 |
531 | Returns content of `mpvremote-filebrowserpaths` option paths indexed.
532 |
533 | Response JSON:
534 |
535 | ```json
536 | [
537 | {
538 | "index": 0,
539 | "path": "/home/usr/media1"
540 | },
541 | {
542 | "index": 1,
543 | "path": "/home/usr/media2"
544 | },
545 | {
546 | "index": 2,
547 | "path": "/home/usr/media3"
548 | }
549 | ]
550 | ```
551 |
552 | ## /api/v1/filebrowser/browse
553 |
554 | **Methods:** POST
555 |
556 | Browse a path or collection.
557 |
558 | **Request JSON:**
559 |
560 | ```json
561 | "path": "/home/usr/media2"
562 | // Or
563 | "collection_id": 1
564 | ```
565 |
566 | ## /api/v1/filebrowser/browse/:index
567 |
568 | **Methods:** GET
569 |
570 | **Note:** Only MPV supported fileformats will return. [Supported file formats](https://github.com/husudosu/mpv-remote-node/blob/master/fileformats.js)
571 |
572 | Returns files with types:
573 |
574 | ```json
575 | {
576 | "content": [
577 | {
578 | "priority": 1,
579 | "type": "directory",
580 | "name": "Feliratok",
581 | "fullPath": "V:\\anime\\Dorohedoro S01+OVA (BD 1080p)\\Feliratok",
582 | "lastModified": "2021-05-03T19:10:16.008Z"
583 | },
584 | {
585 | "priority": 2,
586 | "type": "video",
587 | "name": "Dorohedoro 01 - Caiman.mkv",
588 | "fullPath": "V:\\anime\\Dorohedoro S01+OVA (BD 1080p)\\Dorohedoro 01 - Caiman.mkv",
589 | "lastModified": "2021-05-03T19:09:21.857Z",
590 | "mediaStatus": {
591 | // Media status appears only when localdb enabled!
592 | "id": 2,
593 | "directory": "V:\\anime\\Dorohedoro S01+OVA (BD 1080p)",
594 | "file_name": "Dorohedoro 01 - Caiman.mkv",
595 | "current_time": 1422.0545,
596 | "finished": 1
597 | }
598 | },
599 | {
600 | "priority": 2,
601 | "type": "video",
602 | "name": "Dorohedoro 02 - In the Bag - Eat Quietly During Meals - My Neighbor the Sorcerer.mkv",
603 | "fullPath": "V:\\anime\\Dorohedoro S01+OVA (BD 1080p)\\Dorohedoro 02 - In the Bag - Eat Quietly During Meals - My Neighbor the Sorcerer.mkv",
604 | "lastModified": "2021-05-03T19:04:05.410Z",
605 | "mediaStatus": {
606 | "id": 1,
607 | "directory": "V:\\anime\\Dorohedoro S01+OVA (BD 1080p)",
608 | "file_name": "Dorohedoro 02 - In the Bag - Eat Quietly During Meals - My Neighbor the Sorcerer.mkv",
609 | "current_time": 596.853,
610 | "finished": 0
611 | }
612 | },
613 | {
614 | "priority": 2,
615 | "type": "video",
616 | "name": "Dorohedoro 03 - Night of the Dead ~ Duel! ~ In Front of the Main Department Store.mkv",
617 | "fullPath": "V:\\anime\\Dorohedoro S01+OVA (BD 1080p)\\Dorohedoro 03 - Night of the Dead ~ Duel! ~ In Front of the Main Department Store.mkv",
618 | "lastModified": "2021-05-03T19:04:26.434Z"
619 | }
620 | ],
621 | "dirname": "Dorohedoro S01+OVA (BD 1080p)",
622 | "prevDir": "V:\\anime",
623 | "cwd": "V:\\anime\\Dorohedoro S01+OVA (BD 1080p)"
624 | }
625 | ```
626 |
627 | ## /api/v1/drives
628 |
629 | Gets drives from server.
630 | Only if `mpvremote-unsafefilebrowsing=1`.
631 |
632 | If unsafe filebrowsing is disabled returns an error.
633 |
634 | Note for Linux users: snap, flatpak virtual drives will be excluded!
635 |
636 | **Response JSON:**
637 |
638 | ```json
639 | // Windows Example
640 | [
641 | {
642 | "path": "C"
643 | },
644 | {
645 | "path": "D:"
646 | }
647 | ]
648 | // Unix systems
649 | [
650 | {
651 | "path": "/"
652 | },
653 | {
654 | "path": "/home"
655 | }
656 | ]
657 | ```
658 |
659 | # Collections
660 |
661 | Local collection handling. Collections only works if you have enabled `mpvremote-uselocaldb`
662 | Collections entries only can be opened if `mpvremote-filebrowserpaths` contains the required paths.
663 |
664 | ## /api/v1/collections
665 |
666 | **Methods:** GET, POST
667 |
668 | ### **GET**
669 |
670 | Get all collections
671 |
672 | **Response JSON:**
673 |
674 | Array of collection.
675 |
676 | ### **POST**
677 |
678 | Creates a collection
679 |
680 | **type** variable/enum created for future use:
681 |
682 | - 1: Movies
683 | - 2: TVShows
684 | - 3: Music
685 |
686 | **Request JSON:**
687 |
688 | ```
689 | "name": "Anime",
690 | "type": 1,
691 | "paths": [
692 | {
693 | "path": "/home/usr/media1"
694 | },
695 | {
696 | "path": "/home/usr/media2"
697 | }
698 | ]
699 | ```
700 |
701 | ## /api/v1/collections/:id
702 |
703 | **Methods:** GET, PATCH, DELETE
704 |
705 | ### **GET**
706 |
707 | Gets collection
708 |
709 | **Response JSON:**
710 |
711 | ```json
712 | {
713 | "id": 1,
714 | "name": "Anime",
715 | "type": 1,
716 | "paths": [
717 | {
718 | "id": 1
719 | "path": "/home/usr/media1"
720 | },
721 | {
722 | "id": 2,
723 | "path": "/home/usr/media2"
724 | }
725 | ]
726 | }
727 | ```
728 |
729 | ### **PATCH**
730 |
731 | Updates collection.
732 |
733 | **Request JSON:**
734 |
735 | ```json
736 | {
737 | "name": "Anime ja nai",
738 | "type": 2,
739 | "paths": [
740 | {
741 | "path": "/home/usr/media3" // Adds new path
742 | },
743 | {
744 | "id": 2,
745 | "path": "/home/usr/media2_other" // Updates existing path
746 | }
747 | ]
748 | }
749 | ```
750 |
751 | ### **DELETE**
752 |
753 | Deletes collection.
754 |
755 | ## /api/v1/collections/entries/:id
756 |
757 | **Methods:** DELETE
758 |
759 | Deletes a collection entry.
760 |
761 | ## /api/v1/collections/:collection_id/entries
762 |
763 | Adds collection entry to collection.
764 |
765 | **Methods:** POST
766 |
767 | **Request JSON:**
768 |
769 | ```json
770 | {
771 | "collection_id": 1,
772 | "path": "/home/usr/media4"
773 | }
774 | ```
775 |
776 | # Computer actions
777 |
778 | ## /api/v1/computer/:action
779 |
780 | **Methods:** POST
781 |
782 | action can be:
783 |
784 | - shutdown
785 | - reboot
786 | - quit
787 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.0.2
4 |
5 | - Socket IO server replaced with HTTP API
6 | - Plugin settings added
7 | - Computer actions implemented like Shutdown/Reboot.
8 | - Ability to change subtitle font size, option to ASS override.
9 | - Get MPV version info and plugin settings via route, can be useful when building your own implementation of frontend.
10 | - Filebrowser entries improved and extended with file/directory last modified date
11 | - Ability to change audio timing
12 | - Installer PowerShell and bash script
13 | - remote.socketio.js renamed to remoteServer.js
14 |
15 | ## 1.0.3
16 |
17 | - Code tidy up
18 | - MPV socket Verbose mode made optional
19 | - Better structure, validation, error handling for collections
20 | - Removed most debug messages from server
21 | - Metadata reading fixed
22 | - Filebrowserpath behaviour fixed
23 | - Hotfix: installer scritps fixed
24 |
25 | ## 1.0.4
26 |
27 | - Load file improvements, way to set http-header-fields, needed for playing data from streaming services.
28 |
29 | ## 1.0.5
30 |
31 | - max-volume added (contribution by: https://github.com/byter11)
32 | - multiple MPV instances supported (set webport and webportrangeend on your settings)
33 | - placing "file-local-options.txt" to OS temp directory
34 |
35 | ## 1.0.6
36 |
37 | - Ability to turn off OSD messages via `mpvremote-osd-messages` setting,
38 | - MPV info now includes version of MPV remote plugin
39 |
40 | ## 1.0.7
41 |
42 | - Fixed IP getting on newer Node.JS versions,
43 | - If the CPU usage high on the host machine returning cached properties,
44 | - Ability to exclude properties on /status route as query param, check [APIDESIGN.MD](https://github.com/husudosu/mpv-remote-node/blob/master/APIDESIGN.md)
45 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2021 Ferenc Nánási
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | MPV remote control API. You can use [MPV Remote android application](https://github.com/husudosu/mpv-remote-app/releases/latest) or you can create your own frontend.
2 |
3 | [API documentation accessible here.](https://github.com/husudosu/mpv-remote-node/blob/master/APIDESIGN.md)
4 |
5 | # Installation
6 |
7 | ### Requirements:
8 |
9 | - [Node.JS](https://nodejs.org/en/) (use [13.14.0 LTS](https://nodejs.org/download/release/v13.14.0/) for Windows 7)
10 | - [yt-dlp](https://github.com/yt-dlp/yt-dlp) or [youtube-dl](https://youtube-dl.org/) for playing youtube videos,
11 |
12 | ### Install package
13 |
14 | **Linux:**
15 |
16 | Open your favorite terminal and:
17 |
18 | ```bash
19 | sudo npm install -g mpv-remote
20 | mpv-remote # Follow instructions
21 | ```
22 |
23 | **Windows:**
24 | Open powershell as admin. It's required only for creating symbolic links.
25 |
26 | ```powershell
27 | Set-ExecutionPolicy Unrestricted -Force # Allows running PS scripts from unknown sources
28 | npm install -g mpv-remote
29 | mpv-remote # Follow instructions
30 | ```
31 |
32 | ## Update
33 |
34 | You have to re-run installation script after updating the package.
35 |
36 | ```bash
37 | npm update -g mpv-remote
38 | mpv-remote # Follow instructions
39 | ```
40 |
41 | Note: if you get "Cannot create symbolic link because the path already exists" don't worry the installation will be fine.
42 |
43 | # How to run MPV
44 |
45 | If you don't want MPV close after playback finished use --idle flag or you can add `idle=yes` to your mpv.conf.
46 |
47 | ```
48 | mpv --idle
49 | ```
50 |
51 | # Configuration variables
52 |
53 | You can configure server by using `--script-opts` flag of MPV like this (options seperated by ,):
54 |
55 | ```
56 | mpv --script-opts=mpvremote-filebrowserpaths=/home/sudosu,mpvremote-uselocaldb=0
57 | ```
58 |
59 | Or you can use a script-opts file.
60 |
61 | scipt-opts location for mpvremote:
62 |
63 | ```bash
64 | %appdata%/mpv/script-opts/mpvremote.conf # For windows
65 | ~/.config/mpv/script-opts/mpvremote.conf # For linux
66 | ```
67 |
68 | If using script-opts file, you don't need `mpvremote-` prefix at configuration options.
69 |
70 | [More info about script-opts files.](https://mpv.io/manual/master/#configuration)
71 |
72 | Example configuration file:
73 |
74 | ```
75 | # ~/.config/mpv/script-opts/mpvremote.conf
76 | uselocaldb=1
77 | webport=8000
78 | webportrangeend=8010
79 | unsafefilebrowsing=1
80 | filebrowserpaths="'V:\anime';'W:\anime';'W:\Steins;Gate'"
81 | verbose=0
82 | osd-messages=1
83 | ```
84 |
85 | ## Available options:
86 |
87 | | Option name | Description | Default value | Available options/example |
88 | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ---------------------------------------- |
89 | | mpvremote-uselocaldb | Use local database to store media statuses and collections. | 1 | 0 - Disabled
1 - Enabled |
90 | | mpvremote-filebrowserpaths | Stores paths which can be browsable by users it's a semicolon seperated list | N/A | "'V:\anime';'W:\anime';'W:\Steins;Gate'" |
91 | | mpvremote-webport | This option gonna be your first available port for MPV. | 8000 | Any port within correct range |
92 | | mpvremote-webportrangeend | Web port range end. if mpvremote-webport is 8000 and this option set to 8004, 5 instances of MPV gonna be supported. Increase/decrease these numbers as you wish. | 8004 | Any port within correct range |
93 | | mpvreomte-address | Server address | Your first local IP | 127.0.0.1 |
94 | | mpvremote-unsafefilebrowsing | Allows you to browse your local filesystem. Be careful though, exposing your whole filesystem not the safest option. For security reasons filebrowser only send results of media files, playlists, subtitle files and subdirectories. | 1 | 0 - Disabled
1 - Enabled |
95 | | mpvremote-verbose | Verbose logging of MPV socket | 0 | 0 - Disabled
1 - Enabled |
96 | | mpvremote-osd-messages | Show OSD messages on the player created by this plugin | 1 | 0 - Disabled
1 - Enabled |
97 |
98 | # Troubleshooting
99 |
100 | ## NPM install/update takes forever
101 |
102 | Sometimes NPM takes forever to install a package, [it's a known bug](https://github.com/npm/cli/issues/3359), try update NPM to the latest version and hope it's going to work. Run this as administrator:
103 |
104 | ```
105 | npm install -g npm@latest
106 | ```
107 |
108 | ## Server not starting
109 |
110 | If the server not starts, try run it manually, to get the exception (From terminal/command prompt):
111 |
112 | ```bash
113 | node ~/.config/mpv/scripts/mpvremote/remoteServer.js # On linux systems
114 | node %APPDATA%/mpv/scripts/mpvremote/remoteServer.js # On Windows from command prompt.
115 | ```
116 |
117 | If you report server non starting issue copy the output of this command.
118 |
119 | If you get "No socket provided" output the server works fine, so there's something up with the plugin or MPV itself.
120 |
121 | ## Youtube playback issues
122 |
123 | I recommend using [yt-dlp](https://github.com/yt-dlp/yt-dlp) for playing Youtube videos, but if you use youtube-dl:
124 |
125 | - If you can't play Youtube videos then try to update the **youtube-dl** package (as admin): `pip3 install --upgrade youtube-dl`
126 |
127 | ## Common issues on Linux
128 |
129 | yargs requires 12 or newer version of Node.JS so you should update your Node.JS version. For example this error occours on Ubuntu 20.04.3 LTS.
130 |
131 | - [How to update Node.JS](https://askubuntu.com/questions/426750/how-can-i-update-my-nodejs-to-the-latest-version)
132 |
133 | If the server works fine, then there's an issue with MPV itself. Some linux distributions like Debian and MX Linux ships pre-built MPV packages without Javascript support.
134 |
135 | You can check it by using this command:
136 |
137 | ```bash
138 | mpv -v | grep javascript
139 | ```
140 |
141 | if the output is empty, there's no javascript support.
142 |
143 | Install mujs and [build MPV for yourself](https://github.com/mpv-player/mpv-build)
144 |
145 | ## When you report an issue
146 |
147 | It makes problem solving easier if you provide some info about your environment.
148 |
149 | - Your OS,
150 | - Node.JS version (`node -v`)
151 | - NPM version (`npm -v`)
152 | - MPV version (`mpv -v`)
153 |
154 | # TODO
155 |
156 | - Need better installation scripts for Windows and Linux, where update can be handled easily. (NPM postinstall script maybe)
157 | - Improve API documentation,
158 | - Better installation scripts,
159 | - Make a Youtube video about installing/updating MPV-remote on Windows and Linux
160 |
161 | # Disclaimer
162 |
163 | The app developer DOES NOT promotes piracy! Other apps and modules used by this app may include some level of piracy.
164 |
--------------------------------------------------------------------------------
/bin.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | /*
3 | Launcher came from here.
4 | https://github.com/mrxdst/webtorrent-mpv-hook/blob/master/src/bin.ts
5 | */
6 | const path = require("path");
7 | const os = require("os");
8 |
9 | const pluginDir = path.join(__dirname, "mpvremote");
10 | const MPVHome = getMPVHome();
11 | const scriptFolder = path.join(MPVHome, "scripts");
12 | const scriptOptsFolder = path.join(MPVHome, "script-opts");
13 |
14 | const target = path.join(__dirname, "remoteServer.js");
15 | const target1 = path.join(__dirname, "watchlisthandler.js");
16 | const target2 = path.join(__dirname, "mpvremote.conf");
17 | const powershellInstaller = path.join(__dirname, "install.ps1");
18 | const bashInstaller = path.join(__dirname, "install.sh");
19 |
20 | const link = path.join(scriptFolder, "mpvremote", "remoteServer.js");
21 | const link1 = path.join(scriptFolder, "mpvremote", "watchlisthandler.js");
22 |
23 | const readline = require("readline");
24 | const rl = readline.createInterface({
25 | input: process.stdin,
26 | output: process.stdout,
27 | });
28 |
29 | function printInsturctions() {
30 | console.log(
31 | [
32 | `mpv-remote`,
33 | "",
34 | `${
35 | os.platform() === "win32"
36 | ? "On Windows you can't create symlink without Administrator privileges!"
37 | : ""
38 | }`,
39 | "First copy mpvremote plugin to your MPV plugins folder:",
40 | "",
41 | ` ${
42 | os.platform() === "win32"
43 | ? `xcopy /i "${pluginDir}" "${path.join(scriptFolder, "mpvremote")}"`
44 | : `mkdir -p "${scriptFolder}" && cp -r "${pluginDir}" "${scriptFolder}"`
45 | }`,
46 | "",
47 | "You need to symlink the script file to your MPV scripts folder:",
48 | "",
49 | ` ${
50 | os.platform() === "win32"
51 | ? `mklink "${link}" "${target}"\n or\n New-Item -ItemType SymbolicLink -Path "${link}" -Target "${target}"`
52 | : `ln -s "${target}" "${link}"`
53 | }`,
54 | "Copy default config file by using:",
55 | ` ${
56 | os.platform() === "win32"
57 | ? `echo f | xcopy /f /y "${target2}" "${scriptOptsFolder}"`
58 | : `mkdir -p "${scriptOptsFolder}" && cp -r "${target2}" "${scriptOptsFolder}"`
59 | }`,
60 | "If you want save media status do this:",
61 | ` ${
62 | os.platform() === "win32"
63 | ? `mklink "${link1}" "${target1}"\n or\n New-Item -ItemType SymbolicLink -Path "${link1}" -Target "${target1}"`
64 | : `ln -s "${target1}" "${link1}"`
65 | }`,
66 | "Download the Android app here: https://github.com/husudosu/mpv-remote-app/blob/master/android/app/release/app-release.apk",
67 | ].join("\n")
68 | );
69 | }
70 |
71 | function getMPVHome() {
72 | let mpvHome;
73 |
74 | if (os.platform() === "win32") {
75 | mpvHome = process.env["MPV_HOME"] || "%APPDATA%/mpv";
76 | } else {
77 | mpvHome = process.env["MPV_HOME"];
78 | if (!mpvHome) {
79 | const xdgConfigHome = process.env["XDG_CONFIG_HOME"] || "$HOME/.config";
80 | mpvHome = path.join(xdgConfigHome, "mpv");
81 | }
82 | }
83 | return mpvHome;
84 | }
85 |
86 | function automatedInstaller() {
87 | // If Win32 check if user runs powershell
88 | if (os.platform() === "win32") {
89 | console.log(
90 | `Open PowerShell as admin and run this command: ${powershellInstaller}`
91 | );
92 | } else {
93 | console.log(`Run this script: ${bashInstaller}`);
94 | }
95 | }
96 |
97 | rl.question("Would you like use wizzard installer? [Y/N]:", (answer) => {
98 | answer = answer.toUpperCase();
99 | switch (answer) {
100 | case "Y":
101 | automatedInstaller();
102 | rl.close();
103 | break;
104 | case "N":
105 | default:
106 | printInsturctions();
107 | rl.close();
108 | break;
109 | }
110 | });
111 |
--------------------------------------------------------------------------------
/collections.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const {
4 | createCollection,
5 | getCollections,
6 | updateCollection,
7 | deleteCollection,
8 | createCollectionEntry,
9 | deleteCollectionEntry,
10 | NotFoundException,
11 | } = require("./crud");
12 |
13 | const { settings } = require("./settings");
14 |
15 | // Base URL: /api/v1/collections
16 |
17 | router.use(function checkUseLocalDB(req, res, next) {
18 | if (!settings.uselocaldb) {
19 | return res.status(400).json({
20 | error: "mpvremote-uselocaldb disabled!",
21 | });
22 | } else {
23 | next();
24 | }
25 | });
26 |
27 | router.get("/:id?", async (req, res) => {
28 | try {
29 | if (req.params.id) {
30 | const collection = await getCollections(req.params.id);
31 | if (!collection)
32 | return res.status(404).json({ message: "Collection not exists" });
33 | return res.json(collection);
34 | } else {
35 | return res.json(await getCollections());
36 | }
37 | } catch (exc) {
38 | console.log(exc);
39 | return res.status(500).json({ message: exc });
40 | }
41 | });
42 |
43 | router.post("", async (req, res) => {
44 | // TODO Some validation.
45 | try {
46 | const collection = await createCollection(req.body);
47 | return res.json(collection);
48 | } catch (exc) {
49 | console.log(exc);
50 | return res.status(500).json({ message: exc });
51 | }
52 | });
53 |
54 | router.patch("/:collection_id/", async (req, res) => {
55 | try {
56 | return res.json(await updateCollection(req.params.collection_id, req.body));
57 | } catch (exc) {
58 | if (exc instanceof NotFoundException)
59 | return res.status(404).json({ message: exc.message });
60 | else {
61 | console.log(exc);
62 | return res.status(500).json({ message: exc });
63 | }
64 | }
65 | });
66 |
67 | router.delete("/:collection_id/", async (req, res) => {
68 | try {
69 | const collection_id = req.params.collection_id;
70 | deleteCollection(collection_id);
71 | return res.json({});
72 | } catch (exc) {
73 | return res.status(500).json({ message: exc });
74 | }
75 | });
76 |
77 | router.post("/:collection_id/entries/", async (req, res) => {
78 | try {
79 | const collection_entry = await createCollectionEntry(
80 | req.params.collection_id,
81 | req.body
82 | );
83 | return res.json(collection_entry);
84 | } catch (exc) {
85 | if (exc instanceof NotFoundException)
86 | return res.status(404).json({ message: exc.message });
87 | else return res.status(500).json({ message: exc });
88 | }
89 | });
90 |
91 | router.delete("/entries/:id", async (req, res) => {
92 | try {
93 | deleteCollectionEntry(req.params.id);
94 | return res.json({});
95 | } catch (exc) {
96 | if (exc instanceof NotFoundException)
97 | return res.status(404).json({ message: exc.message });
98 | else return res.status(500).json({ message: exc });
99 | }
100 | });
101 |
102 | module.exports = router;
103 |
--------------------------------------------------------------------------------
/crud.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const os = require("os");
3 |
4 | const sqlite3 = require("sqlite3");
5 | const { open } = require("sqlite");
6 | const DB_PATH = path.join(getScriptFolder(), "mpvremote", "remote.db");
7 | const fs = require("fs");
8 |
9 | let db;
10 |
11 | function NotFoundException(message) {
12 | this.message = message || "Object not found";
13 | this.name = "NotFoundException";
14 | }
15 |
16 | function getMPVHome() {
17 | let mpvHome;
18 |
19 | if (os.platform() === "win32") {
20 | mpvHome =
21 | process.env["MPV_HOME"] ||
22 | path.join(os.homedir(), "AppData", "Roaming", "mpv");
23 | } else {
24 | mpvHome = process.env["MPV_HOME"];
25 | if (!mpvHome) {
26 | const xdgConfigHome =
27 | process.env["XDG_CONFIG_HOME"] || `${os.homedir()}/.config`;
28 | mpvHome = path.join(xdgConfigHome, "mpv");
29 | }
30 | }
31 | return mpvHome;
32 | }
33 |
34 | // Get scripts folder
35 | function getScriptFolder() {
36 |
37 | return path.join(getMPVHome(), "scripts");
38 | }
39 |
40 | async function init_tables() {
41 | // Collections
42 | // TYPE Can be: Movies - 1, TVShows - 2, Music - 3
43 | await db.exec(
44 | `CREATE TABLE IF NOT EXISTS collection(
45 | id INTEGER PRIMARY KEY ASC, name TEXT NOT NULL, type INTEGER NOT NULL
46 | )`
47 | );
48 |
49 | // Collection entry
50 | await db.exec(
51 | `CREATE TABLE IF NOT EXISTS collection_entry(
52 | id INTEGER PRIMARY KEY ASC,
53 | collection_id INTEGER NOT NULL,
54 | path TEXT NOT NULL,
55 | CONSTRAINT fk_collection
56 | FOREIGN KEY (collection_id)
57 | REFERENCES collection(id)
58 | ON DELETE CASCADE
59 | )`
60 | );
61 |
62 | // Media status
63 | await db.exec(
64 | `CREATE TABLE IF NOT EXISTS mediastatus(
65 | id INTEGER PRIMARY KEY ASC,
66 | directory TEXT,
67 | file_name TEXT NOT NULL,
68 | current_time REAL,
69 | finished INTEGER
70 | )`
71 | );
72 | }
73 |
74 | async function initDB() {
75 | db = await open({
76 | filename: DB_PATH,
77 | driver: sqlite3.Database,
78 | });
79 | await db.get("PRAGMA foreign_keys=on;");
80 | await init_tables();
81 | }
82 |
83 | async function getMediastatusEntries(filepath = null, directory = null) {
84 | /*
85 | filepath: Gets entry for a single file path
86 | directory: Gets entries for a directory
87 | */
88 | try {
89 | if (filepath != null) {
90 | // If last char is path.sep remove it
91 | if (filepath[filepath.length - 1] == path.sep)
92 | filepath = filepath.slice(0, -1);
93 | let spl = filepath.split(path.sep);
94 | const fileName = spl[spl.length - 1];
95 | spl.pop();
96 |
97 | const directory = spl.join(path.sep);
98 | return await db.get(
99 | "SELECT * FROM mediastatus WHERE directory=? AND file_name=? ORDER BY file_name",
100 | [directory, fileName]
101 | );
102 | } else if (directory != null) {
103 | // directory = directory.split(path.sep);
104 | if (directory[directory.length - 1] == path.sep)
105 | directory = directory.slice(0, -1);
106 | const entries = await db.all(
107 | "SELECT * FROM mediastatus WHERE directory=? ORDER BY file_name",
108 | [directory]
109 | );
110 | return entries;
111 | } else {
112 | return await db.all("SELECT * FROM mediastatus");
113 | }
114 | } catch (exc) {
115 | console.log(exc);
116 | }
117 | }
118 |
119 | async function createMediaStatusEntry(filepath, time, finished) {
120 | try {
121 | const statusEntry = await getMediastatusEntries(filepath);
122 |
123 | let spl = filepath.split(path.sep);
124 | const fileName = spl[spl.length - 1];
125 | spl.pop();
126 |
127 | const directory = spl.join(path.sep);
128 |
129 | // Update status
130 | if (statusEntry) {
131 | await db.run(
132 | "UPDATE mediastatus set current_time=?, finished=? WHERE directory=? AND file_name=?",
133 | [time, finished, directory, fileName]
134 | );
135 | } else {
136 | await db.run(
137 | "INSERT INTO mediastatus (current_time, finished, directory, file_name) VALUES (?, ?, ?, ?)",
138 | [time, finished, directory, fileName]
139 | );
140 | }
141 | } catch (exc) {
142 | console.log(exc);
143 | }
144 | }
145 |
146 | async function addMediaStatusEntry(filepath, time, percentPos) {
147 | /*
148 | If percentPos 90% consider file finished
149 | If <= 5% don't save status to database.
150 | */
151 | let finished = 0;
152 | percentPos = parseFloat(percentPos);
153 | time = parseFloat(time);
154 |
155 | if (percentPos >= 90) finished = 1;
156 | else if (percentPos <= 5) return;
157 |
158 | await createMediaStatusEntry(filepath, time, finished);
159 | // Check if entry already exists
160 | }
161 |
162 | /*
163 | ***
164 | COLLECTIONS CRUD
165 | ***
166 | */
167 | function validateEntry(data) {
168 | if (!fs.existsSync(data.path)) {
169 | throw new NotFoundException(`${data.path} not exists.`);
170 | }
171 | }
172 |
173 | async function createCollection(data) {
174 | // Validate entry path
175 | // if (data.paths && data.paths.length > 0) {
176 | // data.paths.forEach((el) => {
177 | // validateEntry(el);
178 | // });
179 | // }
180 |
181 | const dbres = await db.run(
182 | "INSERT INTO collection (name, type) VALUES (?, ?)",
183 | data.name,
184 | data.type || 1
185 | );
186 |
187 | // Get new object
188 | let collection = await db.get(
189 | "SELECT * FROM collection WHERE id=?",
190 | dbres.lastID
191 | );
192 | collection.paths = [];
193 | if (data.paths && data.paths.length > 0) {
194 | data.paths.forEach(async (element) => {
195 | const entry = await createCollectionEntry(collection.id, element);
196 | collection.paths.push(entry);
197 | });
198 | }
199 |
200 | return collection;
201 | }
202 |
203 | async function getCollections(id = null) {
204 | if (id) {
205 | let collection = await db.get("SELECT * FROM collection WHERE id=?", id);
206 |
207 | if (collection) {
208 | collection.paths = await getCollectionEntries(collection.id);
209 | return collection;
210 | } else {
211 | return null;
212 | }
213 | } else {
214 | let collections = await db.all("SELECT * FROM collection");
215 | return collections;
216 | }
217 | }
218 |
219 | async function updateCollection(id, data) {
220 | // Validate entry paths.
221 | // TODO: Rollbacking on validation error would be better.
222 | // if (data.paths && data.paths.length > 0) {
223 | // data.paths.forEach((el) => {
224 | // validateEntry(el);
225 | // });
226 | // }
227 |
228 | let collection = await db.get("SELECT * FROM collection WHERE id=?", id);
229 | if (!collection) throw new NotFoundException("Collection not exists.");
230 | // Update collection
231 | await db.run(
232 | "UPDATE collection SET name=COALESCE(?, name), type=COALESCE(?, type) WHERE id=?",
233 | [data.name, data.type, id]
234 | );
235 | // Update paths
236 | if (data.paths) {
237 | data.paths.forEach(async (element) => {
238 | // Add collection entry
239 | if (!element.id) await createCollectionEntry(collection.id, element);
240 | // Update path
241 | else await updateCollectionEntry(element.id, element);
242 | });
243 | }
244 | return await getCollections(id);
245 | }
246 |
247 | async function deleteCollection(id) {
248 | const collection = getCollections(id);
249 | if (!collection) throw new NotFoundException("Collection not exists.");
250 | await db.run("DELETE FROM collection WHERE id=?", id);
251 | }
252 |
253 | /*
254 | ***
255 | COLLECTION ENTIRES CRUD
256 | ***
257 | */
258 | async function createCollectionEntry(collection_id, data) {
259 | // Check if collection exists
260 | const collectionExists = await getCollections(collection_id);
261 | if (!collectionExists) throw new NotFoundException("Collection not exists.");
262 |
263 | const dbres = await db.run(
264 | "INSERT INTO collection_entry (collection_id, path) VALUES (?, ?)",
265 | collection_id,
266 | data.path
267 | );
268 | const collection_entry = await db.get(
269 | "SELECT * FROM collection_entry WHERE id=?",
270 | dbres.lastID
271 | );
272 | return collection_entry;
273 | }
274 |
275 | async function getCollectionEntries(collection_id) {
276 | return await db.all(
277 | "SELECT * FROM collection_entry WHERE collection_id=?",
278 | collection_id
279 | );
280 | }
281 |
282 | async function getCollectionEntry(id) {
283 | return await db.get("SELECT * FROM collection_entry WHERE id=?", id);
284 | }
285 |
286 | async function updateCollectionEntry(id, data) {
287 | const collectionEntry = await getCollectionEntry(id);
288 | if (!collectionEntry)
289 | throw new NotFoundException("Collection entry not exists.");
290 | await db.run(
291 | "UPDATE collection_entry SET path=COALESCE(?, path) WHERE id=?",
292 | [data.path, id]
293 | );
294 |
295 | return await getCollectionEntry(id);
296 | }
297 |
298 | async function deleteCollectionEntry(id) {
299 | const collectionEntry = await getCollectionEntry(id);
300 | if (!collectionEntry)
301 | throw new NotFoundException("Collection entry not exists.");
302 | await db.run("DELETE FROM collection_entry WHERE id=?", id);
303 | }
304 |
305 | // Exceptions
306 | exports.NotFoundException = NotFoundException;
307 |
308 | exports.initDB = initDB;
309 | // Media status entries
310 | exports.addMediaStatusEntry = addMediaStatusEntry;
311 | exports.getMediastatusEntries = getMediastatusEntries;
312 |
313 | // Collections
314 | exports.createCollection = createCollection;
315 | exports.getCollections = getCollections;
316 | exports.updateCollection = updateCollection;
317 | exports.deleteCollection = deleteCollection;
318 |
319 | // Collection Entries
320 | exports.createCollectionEntry = createCollectionEntry;
321 | exports.getCollectionEntries = getCollectionEntries;
322 | exports.deleteCollectionEntry = deleteCollectionEntry;
323 | exports.updateCollection = updateCollection;
324 |
325 |
326 | // Get script folder
327 | exports.getScriptFolder = getScriptFolder;
328 | // Get mpv home folder
329 | exports.getMPVHome = getMPVHome;
330 |
--------------------------------------------------------------------------------
/filebrowser.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const fs_async = require("fs").promises;
3 | const path = require("path");
4 |
5 | const express = require("express");
6 | const router = express.Router();
7 | const nodeDiskInfo = require("node-disk-info");
8 |
9 | const { getMediastatusEntries, getCollections } = require("./crud");
10 | const { settings } = require("./settings");
11 | const FILE_FORMATS = require("./fileformats").FILE_FORMATS;
12 |
13 | function detectFileType(extension) {
14 | extension = extension.toLowerCase();
15 |
16 | if (FILE_FORMATS.video.includes(extension)) {
17 | return "video";
18 | } else if (FILE_FORMATS.audio.includes(extension)) {
19 | return "audio";
20 | } else if (FILE_FORMATS.subtitle.includes(extension)) {
21 | return "subtitle";
22 | } else {
23 | return "file";
24 | }
25 | }
26 |
27 | async function getDirectoryContents(qpath) {
28 | // TODO Handle exceptions
29 | let content = [];
30 |
31 | // Add path seperator to qpath end
32 | /*
33 | Interesting because C: (System partition) needs a path seperator to work correctly,
34 | but for my network drives works without path sep.
35 | */
36 | if (qpath[qpath.length - 1] != path.sep) qpath += path.sep;
37 | let mediaStatus = [];
38 |
39 | if (settings.uselocaldb)
40 | mediaStatus = await getMediastatusEntries(null, qpath);
41 |
42 | for (const item of await fs_async.readdir(qpath)) {
43 | try {
44 | if (fs.lstatSync(path.join(qpath, item)).isDirectory()) {
45 | let entry = {
46 | priority: 1,
47 | type: "directory",
48 | name: item,
49 | fullPath: path.join(qpath, item),
50 | };
51 | entry.lastModified = await fs_async
52 | .stat(entry.fullPath)
53 | .then((stat) => stat.mtime)
54 | .catch(() => null);
55 |
56 | content.push(entry);
57 | } else {
58 | let fileType = detectFileType(path.extname(item));
59 | // Render only media, sub types.
60 | if (fileType !== "file") {
61 | let entry = {
62 | priority: 2,
63 | type: fileType,
64 | name: item,
65 | fullPath: path.join(qpath, item),
66 | };
67 | entry.lastModified = await fs_async
68 | .stat(entry.fullPath)
69 | .then((stat) => stat.mtime)
70 | .catch(() => null);
71 | if (settings.uselocaldb)
72 | entry.mediaStatus = mediaStatus.find((el) => el.file_name == item);
73 |
74 | content.push(entry);
75 | }
76 | }
77 | } catch (exc) {
78 | console.log(exc);
79 | }
80 | }
81 | return content;
82 | }
83 |
84 | router.get("/api/v1/drives", async (req, res) => {
85 | try {
86 | if (settings.unsafefilebrowsing) {
87 | let disks = await nodeDiskInfo.getDiskInfo();
88 | // ignore snap, flatpak stuff linux
89 | disks = disks.filter(
90 | (disk) =>
91 | !disk._mounted.includes("snap") && !disk._mounted.includes("flatpak")
92 | );
93 | disks = disks.map((disk) => {
94 | return {
95 | path: disk._mounted,
96 | };
97 | });
98 | return res.json(disks);
99 | } else
100 | return res
101 | .status(403)
102 | .json({ message: "mpvremote-unsafefilebrowsing disabled!" });
103 | } catch (e) {
104 | console.log(e);
105 | res.status(500).json({ message: exc });
106 | }
107 | });
108 |
109 | router.get("/api/v1/filebrowser/paths", async (req, res) => {
110 | try {
111 | return res.json(settings.filebrowserPaths);
112 | } catch (exc) {
113 | console.log(exc);
114 | return res.status(500).json({ message: exc });
115 | }
116 | });
117 |
118 | router.post("/api/v1/filebrowser/browse", async (req, res) => {
119 | try {
120 | let p = req.body.path;
121 | let collectionId = req.body.collection_id;
122 |
123 | // Find FILEBROWSER_PATH entry
124 | if (!p && !collectionId)
125 | return res
126 | .status(400)
127 | .json({ message: "path or collection id missing from request data!" });
128 |
129 | let retval = {};
130 | if (p) {
131 | // If unsafe filebrowsing disabled we've to check FILEBROWSER_PATHS
132 | if (!settings.unsafefilebrowsing) {
133 | // Security: Protect against path-traversal attack by resolving synlinks and ..
134 | p = await fs_async.realpath(p);
135 | let fbe = settings.filebrowserPaths.find((el) => {
136 | return p.startsWith(el.path);
137 | });
138 |
139 | if (!fbe)
140 | return res
141 | .status(400)
142 | .send({ message: `Path not exists on filebrowserpaths: ${p}` });
143 | }
144 |
145 | if (!fs.existsSync(p))
146 | return res.status(404).send({ message: "Path not exists!" });
147 | // Get files from directory
148 |
149 | retval.content = await getDirectoryContents(p);
150 | retval.dirname = path.basename(p);
151 | retval.prevDir = path.resolve(p, "..");
152 | retval.cwd = p;
153 | } else if (collectionId) {
154 | // Get collection contents if local database enabled!
155 | if (!settings.uselocaldb)
156 | return res
157 | .status(400)
158 | .send({ message: "mpvremote-uselocaldb is disabled!" });
159 |
160 | let collection = await getCollections(collectionId);
161 | if (!collection) return res.status(404).send("Collection not exists!");
162 | retval.content = [];
163 | retval.errors = [];
164 | await Promise.all(
165 | collection.paths.map(async (item) => {
166 | // Check if exists on filebrowserpaths
167 | if (!settings.unsafefilebrowsing) {
168 | let fbe = settings.filebrowserPaths.find((el) => {
169 | return item.path.includes(el.path);
170 | });
171 | if (!fbe) {
172 | console.log(`Not exists on filebrowserpaths: ${item.path}`);
173 | retval.errors.push(
174 | `Not exists on filebrowserpaths: ${item.path}`
175 | );
176 | }
177 | } else if (fs.existsSync(item.path)) {
178 | const dir = await getDirectoryContents(item.path);
179 | retval.content = [...retval.content, ...dir];
180 | } else {
181 | console.log(`Path not exists ${item.path}`);
182 | }
183 | })
184 | );
185 | retval.collection_id = collectionId;
186 | }
187 |
188 | // Sort content firstly by priority and alphabet order.
189 | retval.content.sort((a, b) => {
190 | return (
191 | a.priority - b.priority ||
192 | a.name.toLowerCase().localeCompare(b.name.toLowerCase())
193 | );
194 | });
195 |
196 | return res.json(retval);
197 | } catch (exc) {
198 | console.log(exc);
199 | return res.status(500).json({ message: exc });
200 | }
201 | });
202 |
203 | exports.getDirectoryContents = getDirectoryContents;
204 | exports.detectFileType = detectFileType;
205 | module.exports = router;
206 |
--------------------------------------------------------------------------------
/fileformats.js:
--------------------------------------------------------------------------------
1 | const FILE_FORMATS = {
2 | audio: [
3 | ".ac3",
4 | ".a52",
5 | ".eac3",
6 | ".mlp",
7 | ".dts",
8 | ".dts-hd",
9 | ".dtshd",
10 | ".true-hd",
11 | ".thd",
12 | ".truehd",
13 | ".thd+ac3",
14 | ".tta",
15 | ".pcm",
16 | ".wav",
17 | ".aiff",
18 | ".aif",
19 | ".aifc",
20 | ".amr",
21 | ".awb",
22 | ".au",
23 | ".snd",
24 | ".lpcm",
25 | ".ape",
26 | ".wv",
27 | ".shn",
28 | ".adts",
29 | ".adt",
30 | ".mpa",
31 | ".m1a",
32 | ".m2a",
33 | ".mp1",
34 | ".mp2",
35 | ".mp3",
36 | ".m4a",
37 | ".aac",
38 | ".flac",
39 | ".oga",
40 | ".ogg",
41 | ".opus",
42 | ".spx",
43 | ".mka",
44 | ".weba",
45 | ".wma",
46 | ".f4a",
47 | ".ra",
48 | ".ram",
49 | ".3ga",
50 | ".3ga2",
51 | ".ay",
52 | ".gbs",
53 | ".gym",
54 | ".hes",
55 | ".kss",
56 | ".nsf",
57 | ".nsfe",
58 | ".sap",
59 | ".spc",
60 | ".vgm",
61 | ".vgz",
62 | ".cue",
63 | ],
64 | video: [
65 | ".yuv",
66 | ".y4m",
67 | ".m2ts",
68 | ".m2t",
69 | ".mts",
70 | ".mtv",
71 | ".ts",
72 | ".tsv",
73 | ".tsa",
74 | ".tts",
75 | ".trp",
76 | ".mpeg",
77 | ".mpg",
78 | ".mpe",
79 | ".mpeg2",
80 | ".m1v",
81 | ".m2v",
82 | ".mp2v",
83 | ".mpv",
84 | ".mpv2",
85 | ".mod",
86 | ".tod",
87 | ".vob",
88 | ".vro",
89 | ".evob",
90 | ".evo",
91 | ".mpeg4",
92 | ".m4v",
93 | ".mp4",
94 | ".mp4v",
95 | ".mpg4",
96 | ".h264",
97 | ".avc",
98 | ".x264",
99 | ".264",
100 | ".hevc",
101 | ".h265",
102 | ".x265",
103 | ".265",
104 | ".ogv",
105 | ".ogm",
106 | ".ogx",
107 | ".mkv",
108 | ".mk3d",
109 | ".webm",
110 | ".avi",
111 | ".vfw",
112 | ".divx",
113 | ".3iv",
114 | ".xvid",
115 | ".nut",
116 | ".flic",
117 | ".fli",
118 | ".flc",
119 | ".nsv",
120 | ".gxf",
121 | ".mxf",
122 | ".wm",
123 | ".wmv",
124 | ".asf",
125 | ".dvr-ms",
126 | ".dvr",
127 | ".wt",
128 | ".dv",
129 | ".hdv",
130 | ".flv",
131 | ".f4v",
132 | ".qt",
133 | ".mov",
134 | ".hdmov",
135 | ".rm",
136 | ".rmvb",
137 | ".3gpp",
138 | ".3gp",
139 | ".3gp2",
140 | ".3g2",
141 | ".iso",
142 | ],
143 | playlist: [".m3u", ".m3u8", ".pls"],
144 | subtitle: [
145 | ".aqt",
146 | ".cvd",
147 | ".dks",
148 | ".jss",
149 | ".sub",
150 | ".ttxt",
151 | ".mpl",
152 | ".sub",
153 | ".pjs",
154 | ".psb",
155 | ".rt",
156 | ".smi",
157 | ".ssf",
158 | ".srt",
159 | ".ssa",
160 | ".ass",
161 | ".sub",
162 | ".svcd",
163 | ".usf",
164 | ".sub",
165 | ".idx",
166 | ".txt",
167 | ],
168 | };
169 |
170 | exports.FILE_FORMATS = FILE_FORMATS;
171 |
--------------------------------------------------------------------------------
/install.ps1:
--------------------------------------------------------------------------------
1 | # Automated installer for mpvremote Windows version
2 |
3 | $MPV_PATH = "$env:APPDATA/mpv";
4 | if ($Env:MPV_HOME){
5 | $MPV_PATH = $Env:MPV_HOME
6 | }
7 |
8 | # Script path for older Powershell versions.
9 | $scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
10 |
11 | $pluginPath = Join-Path -Path "$scriptPath" -ChildPath "mpvremote"
12 | $scriptOptsPath = Join-Path -Path "$scriptPath" -ChildPath "mpvremote.conf"
13 | $mainPath = Join-Path -Path "$scriptPath" -ChildPath "remoteServer.js"
14 | $watchlistHandlerPath = Join-Path -Path "$scriptPath" -ChildPath "watchlisthandler.js"
15 |
16 | $destPluginPath = Join-Path -Path "$MPV_PATH" -ChildPath "\scripts\mpvremote"
17 | $destScriptOptsPath = Join-Path "$MPV_PATH" -ChildPath "\script-opts\mpvremote.conf"
18 | $destMainPath = Join-Path -Path "$destPluginPath" -ChildPath "remoteServer.js"
19 | $destWatchlistHandlerPath = Join-Path -Path "$destPluginPath" -ChildPath "watchlisthandler.js"
20 |
21 | function Check-IsElevated
22 | {
23 | $id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
24 | $p = New-Object System.Security.Principal.WindowsPrincipal($id)
25 | if ($p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator))
26 | { Write-Output $true }
27 | else
28 | { Write-Output $false }
29 | }
30 |
31 |
32 | if (-NOT(Check-IsElevated)){
33 | "For creating symbolic links, you need run this script as administrator!"
34 | Exit
35 | }
36 |
37 | # Copy base plugin directory
38 | xcopy /i $pluginPath $destPluginPath
39 | # Symlink for remoteServer.js
40 | Remove-Item $destMainPath -ErrorAction Ignore
41 | New-Item -ItemType SymbolicLink -Path $destMainPath -Target $mainPath
42 |
43 | $shouldUseWatchlist = Read-Host "Use watchlist handler? [Y/N](Default:Y)"
44 | if ($shouldUseWatchlist -ne "N"){
45 | Remove-Item $destWatchlistHandlerPath -ErrorAction Ignore
46 | New-Item -ItemType SymbolicLink -Path $destWatchlistHandlerPath -Target $watchlistHandlerPath
47 | }
48 |
49 | $shouldCopyConfig = Read-Host "Copy default config? [Y/N](Default:Y)"
50 |
51 | if ($shouldCopyConfig.ToUpper() -ne "N"){
52 | echo "f" | xcopy /f /y $scriptOptsPath $destscriptOptsPath
53 | }
54 |
55 |
56 | "Wizzard done. MPV remote should launch when running MPV"
57 | "Download the Android app here: https://github.com/husudosu/mpv-remote-app/releases/latest"
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Automated installer for mpvremote Linux version
3 | # Get MPV Path
4 | MPV_PATH=""
5 | if [[ -z "${MPV_HOME}" ]]; then
6 | if [[ -z "${XDG_CONFIG_HOME}" ]]; then
7 | MPV_PATH="${HOME}/.config/mpv"
8 | else
9 | MPV_PATH="${XDG_CONFIG_HOME}/mpv"
10 | fi
11 | else
12 | MPV_PATH=${MPV_HOME}
13 | fi
14 |
15 | bashScriptPath="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
16 | scriptOptsFolder="$MPV_PATH/script-opts"
17 | mpvScriptFolder="$MPV_PATH/scripts"
18 |
19 | pluginPath="$bashScriptPath/mpvremote"
20 | scriptOptsPath="$bashScriptPath/mpvremote.conf"
21 | mainPath="$bashScriptPath/remoteServer.js"
22 | watchlistHandlerPath="$bashScriptPath/watchlisthandler.js"
23 |
24 | destPluginPath="$MPV_PATH/scripts/mpvremote"
25 | destScriptOptsPath="$scriptOptsFolder/mpvremote.conf"
26 | destMainPath="$destPluginPath/remoteServer.js"
27 | destWatchlistHandlerPath="$destPluginPath/watchlisthandler.js"
28 |
29 | mkdir -p "$mpvScriptFolder" && cp -r "$pluginPath" "$mpvScriptFolder"
30 | ln -sf "$mainPath" "$destMainPath"
31 |
32 | copyOpts(){
33 | mkdir -p "$scriptOptsFolder" && cp -r "$scriptOptsPath" "$scriptOptsFolder"
34 | }
35 |
36 | watchlistHandler(){
37 | ln -sf "$watchlistHandlerPath" "$destWatchlistHandlerPath"
38 | }
39 |
40 | while true; do
41 | read -p "Use watchlist handler? [Y/N](Default: Y)" Yn
42 | case $Yn in
43 | [Yy]* ) watchlistHandler;break;;
44 | [Nn]* ) break;;
45 | "") watchlistHandler; break;;
46 | * ) echo "Please answer yes or no.";;
47 | esac
48 | done
49 |
50 | while true; do
51 | read -p "Copy default config? [Y/N](Default: Y)" Yn
52 | case $Yn in
53 | [Yy]* ) copyOpts; break;;
54 | [Nn]* ) break;;
55 | "") copyOpts; break;;
56 | * ) echo "Please answer yes or no.";;
57 | esac
58 | done
59 |
60 | echo "Wizzard done. MPV remote should launch when running MPV"
61 | echo "Download the Android app here: https://github.com/husudosu/mpv-remote-app/releases/latest"
--------------------------------------------------------------------------------
/mpvremote.conf:
--------------------------------------------------------------------------------
1 | uselocaldb=1
2 | webport=8000
3 | webportrangeend=8004
4 | unsafefilebrowsing=1
5 | verbose=0
6 | osd-messages=1
--------------------------------------------------------------------------------
/mpvremote/main.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | // This is the plugin file for MPV
3 |
4 | /*Path handling, script idea came from webtorrent plugin :
5 | https://github.com/mrxdst/webtorrent-mpv-hook
6 | ES5 syntax works only.
7 | */
8 |
9 | var options = {
10 | uselocaldb: 1,
11 | filebrowserpaths: "",
12 | webport: 8000,
13 | webportrangeend: 8005,
14 | address: "",
15 | unsafefilebrowsing: 1,
16 | verbose: 0,
17 | "osd-messages": 1,
18 | };
19 |
20 | mp.options.read_options(options, "mpvremote");
21 |
22 | var platform = mp.utils.getenv("windir") ? "win32" : "unix";
23 | var tempdir = mp.utils.getenv("TEMP") || mp.utils.getenv("TMP") || "/tmp"; // Temp dir
24 | var pathsep = platform === "win32" ? "\\" : "/";
25 |
26 | function createMPVSocketFilename() {
27 | var i = 0;
28 | var fname = "";
29 | while (true) {
30 | fname =
31 | platform === "win32"
32 | ? "\\\\.\\pipe\\mpvremote" + i
33 | : "/tmp/mpvremote" + i;
34 | i++;
35 | if (!mp.utils.file_info(fname)) break;
36 | }
37 | return fname;
38 | }
39 |
40 | function getMPVSocket() {
41 | var socketName = mp.get_property("input-ipc-server");
42 |
43 | if (!socketName) {
44 | var fname = createMPVSocketFilename();
45 |
46 | mp.set_property("input-ipc-server", fname);
47 | // Check socket
48 | socketName = mp.get_property("input-ipc-server");
49 | }
50 |
51 | // TODO raise error if socket still not exists!
52 | return socketName;
53 | }
54 |
55 | function getScriptPath(filename) {
56 | var script = mp.get_script_file().replace(/\\/g, "\\\\");
57 | var p = mp.command_native({
58 | name: "subprocess",
59 | args: ["node", "-p", "require('fs').realpathSync('" + script + "')"],
60 | playback_only: false,
61 | capture_stdout: true,
62 | });
63 | var s = p.stdout.split(pathsep);
64 | s[s.length - 1] = filename;
65 | return s.join(pathsep);
66 | }
67 |
68 | var scriptPath = getScriptPath("remoteServer.js");
69 | var watchlistHandlerPath = getScriptPath("watchlisthandler.js");
70 |
71 | var socketName = getMPVSocket();
72 |
73 | var serverArgs = [
74 | "node",
75 | scriptPath,
76 | socketName,
77 | "-p " + options.webport,
78 | "-e " + options.webportrangeend,
79 | ];
80 |
81 | if (options.address) {
82 | serverArgs.push("--address");
83 | serverArgs.push(options.address);
84 | }
85 | if (options.verbose) serverArgs.push("--verbose");
86 | if (options.uselocaldb) serverArgs.push("--uselocaldb");
87 | if (options.filebrowserpaths.length > 0) {
88 | var pathsArr = options.filebrowserpaths.split("';'");
89 | serverArgs.push("--filebrowserpaths");
90 | for (var i = 0; i < pathsArr.length; i++) {
91 | serverArgs.push(pathsArr[i]);
92 | }
93 | }
94 | if (options.unsafefilebrowsing) serverArgs.push("--unsafefilebrowsing");
95 | if (options["osd-messages"]) serverArgs.push("--osd-messages");
96 |
97 | mp.command_native_async(
98 | {
99 | name: "subprocess",
100 | args: serverArgs,
101 | playback_only: false,
102 | capture_stderr: true,
103 | },
104 | function (success, result, error) {
105 | mp.msg.info(JSON.stringify(success));
106 | mp.msg.info(JSON.stringify(result));
107 | mp.msg.info(JSON.stringify(error));
108 | }
109 | );
110 |
111 | function setFileLocalOptions(options) {
112 | for (var key in options) {
113 | if (options.hasOwnProperty(key)) mp.set_property(key, options[key]);
114 | }
115 | }
116 | /* For handling file-local-option
117 | Currently storing file-local-options via file-local-options.txt
118 | because have to get file-local-options before the file fully loaded.
119 | */
120 | mp.add_hook("on_load", 50, function () {
121 | try {
122 | /*
123 | JSON structure should be something like this:
124 | {
125 | "filename1": {"http-header-fields": ["test: a", "test1: b"]},
126 | "filename2": {"http-header-fields": ["test: a", "test1: b"]}
127 | }
128 | */
129 | mp.set_property("force-media-title", "");
130 | // var scriptDir = mp.get_script_directory();
131 | var fileName = mp.get_property("path");
132 | var p = tempdir + "/" + "file-local-options.txt";
133 | var fileLocalOptions = mp.utils.read_file(p);
134 | fileLocalOptions = JSON.parse(fileLocalOptions);
135 |
136 | // Find filename in the file-local-options
137 | for (var key in fileLocalOptions) {
138 | if (key === fileName) setFileLocalOptions(fileLocalOptions[key]);
139 | }
140 | } catch (exc) {
141 | mp.msg.info(exc);
142 | }
143 | });
144 |
145 | // On unload MPV, need this for saving playbacktime to database
146 | if (options.uselocaldb) {
147 | mp.add_hook("on_unload", 50, function () {
148 | var currentPlaybackTime = mp.get_property("playback-time");
149 | var currentFilename = mp.get_property("path");
150 | var currentPercentPos = mp.get_property("percent-pos");
151 |
152 | /* Calling binary,
153 | not the ideal solution, but I can't find any information regarding hook supporting on JSON-IPC.
154 | Fetch API not supported by MuJS
155 | */
156 | mp.command_native_async({
157 | name: "subprocess",
158 | args: [
159 | "node",
160 | watchlistHandlerPath,
161 | currentFilename,
162 | currentPlaybackTime,
163 | currentPercentPos,
164 | ],
165 | playback_only: false,
166 | capture_stderr: true,
167 | });
168 | mp.msg.info("Mediastatus updated");
169 | });
170 | }
171 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mpv-remote",
3 | "version": "1.0.7",
4 | "description": "MPV remote control",
5 | "main": "./remote.socketio.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "bin": "./bin.js",
10 | "author": "Ferenc Nánási",
11 | "dependencies": {
12 | "cors": "^2.8.5",
13 | "express": "^4.17.1",
14 | "node-disk-info": "^1.3.0",
15 | "node-mpv": "^2.0.0-beta.2",
16 | "portfinder": "^1.0.28",
17 | "sqlite": "^4.0.23",
18 | "sqlite3": "^5.0.2",
19 | "yargs": "^17.2.1"
20 | },
21 | "type": "commonjs",
22 | "keywords": [
23 | "mpv",
24 | "Audio",
25 | "Video",
26 | "Stream",
27 | "multimedia",
28 | "Remote"
29 | ],
30 | "license": "MIT",
31 | "homepage": "https://github.com/husudosu/mpv-remote-node"
32 | }
33 |
--------------------------------------------------------------------------------
/postman/MPV Remote.postman_collection.json:
--------------------------------------------------------------------------------
1 | {
2 | "info": {
3 | "_postman_id": "f303aa5d-e14d-4fb1-99f7-c65c5e50badf",
4 | "name": "MPV Remote",
5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
6 | },
7 | "item": [
8 | {
9 | "name": "Status",
10 | "item": [
11 | {
12 | "name": "MPV Info",
13 | "request": {
14 | "method": "GET",
15 | "header": [],
16 | "url": {
17 | "raw": "{{baseURL}}/mpvinfo",
18 | "host": [
19 | "{{baseURL}}"
20 | ],
21 | "path": [
22 | "mpvinfo"
23 | ]
24 | }
25 | },
26 | "response": [
27 | {
28 | "name": "MPV Info",
29 | "originalRequest": {
30 | "method": "GET",
31 | "header": [],
32 | "url": {
33 | "raw": "{{baseURL}}/mpvinfo",
34 | "host": [
35 | "{{baseURL}}"
36 | ],
37 | "path": [
38 | "mpvinfo"
39 | ]
40 | }
41 | },
42 | "_postman_previewlanguage": "json",
43 | "header": null,
44 | "cookie": [],
45 | "body": "{\r\n \"ffmpeg-version\": \"git-2021-10-10-b4d665bf6\",\r\n \"mpv-version\": \"mpv 0.33.0-360-gb3f3c3fec0\",\r\n \"libass-version\": 22028288,\r\n \"mpvremoteConfig\": {\r\n \"_\": [\r\n \"\\\\\\\\.\\\\pipe\\\\mpvremote\"\r\n ],\r\n \"p\": 8000,\r\n \"webport\": 8000,\r\n \"uselocaldb\": true,\r\n \"unsafefilebrowsing\": true,\r\n \"$0\": \"AppData\\\\Roaming\\\\mpv\\\\scripts\\\\mpvremote\\\\remoteServer.js\"\r\n }\r\n}"
46 | }
47 | ]
48 | },
49 | {
50 | "name": "Playback status",
51 | "request": {
52 | "method": "GET",
53 | "header": [],
54 | "url": {
55 | "raw": "{{baseURL}}/status",
56 | "host": [
57 | "{{baseURL}}"
58 | ],
59 | "path": [
60 | "status"
61 | ]
62 | }
63 | },
64 | "response": [
65 | {
66 | "name": "No active playback",
67 | "originalRequest": {
68 | "method": "GET",
69 | "header": [],
70 | "url": {
71 | "raw": "{{baseURL}}/status",
72 | "host": [
73 | "{{baseURL}}"
74 | ],
75 | "path": [
76 | "status"
77 | ]
78 | }
79 | },
80 | "status": "OK",
81 | "code": 200,
82 | "_postman_previewlanguage": "json",
83 | "header": [
84 | {
85 | "key": "X-Powered-By",
86 | "value": "Express"
87 | },
88 | {
89 | "key": "Access-Control-Allow-Origin",
90 | "value": "*"
91 | },
92 | {
93 | "key": "Content-Type",
94 | "value": "application/json; charset=utf-8"
95 | },
96 | {
97 | "key": "Content-Length",
98 | "value": "300"
99 | },
100 | {
101 | "key": "ETag",
102 | "value": "W/\"12c-EqkziURYVn/MlxExvXBUS8Tj3mg\""
103 | },
104 | {
105 | "key": "Date",
106 | "value": "Sat, 06 Nov 2021 08:18:38 GMT"
107 | },
108 | {
109 | "key": "Connection",
110 | "value": "keep-alive"
111 | },
112 | {
113 | "key": "Keep-Alive",
114 | "value": "timeout=5"
115 | }
116 | ],
117 | "cookie": [],
118 | "body": "{\n \"pause\": false,\n \"mute\": false,\n \"filename\": null,\n \"duration\": 0,\n \"position\": 0,\n \"remaining\": 0,\n \"media-title\": null,\n \"playlist\": [],\n \"chapter\": 0,\n \"chapter-list\": [],\n \"volume\": 100,\n \"fullscreen\": false,\n \"speed\": 1,\n \"sub-delay\": 0,\n \"sub-visibility\": true,\n \"track-list\": [],\n \"audio-delay\": 0,\n \"sub-font-size\": 55,\n \"sub-ass-override\": \"no\"\n}"
119 | },
120 | {
121 | "name": "Active playback",
122 | "originalRequest": {
123 | "method": "GET",
124 | "header": [],
125 | "url": {
126 | "raw": "{{baseURL}}/status",
127 | "host": [
128 | "{{baseURL}}"
129 | ],
130 | "path": [
131 | "status"
132 | ]
133 | }
134 | },
135 | "status": "OK",
136 | "code": 200,
137 | "_postman_previewlanguage": "json",
138 | "header": [
139 | {
140 | "key": "X-Powered-By",
141 | "value": "Express"
142 | },
143 | {
144 | "key": "Access-Control-Allow-Origin",
145 | "value": "*"
146 | },
147 | {
148 | "key": "Content-Type",
149 | "value": "application/json; charset=utf-8"
150 | },
151 | {
152 | "key": "Content-Length",
153 | "value": "1773"
154 | },
155 | {
156 | "key": "ETag",
157 | "value": "W/\"6ed-XFDKN7pIkjXe/MozYKQiDZDb9SU\""
158 | },
159 | {
160 | "key": "Date",
161 | "value": "Sat, 06 Nov 2021 08:26:18 GMT"
162 | },
163 | {
164 | "key": "Connection",
165 | "value": "keep-alive"
166 | },
167 | {
168 | "key": "Keep-Alive",
169 | "value": "timeout=5"
170 | }
171 | ],
172 | "cookie": [],
173 | "body": "{\n \"pause\": true,\n \"mute\": false,\n \"filename\": \"Dorohedoro 02 - In the Bag - Eat Quietly During Meals - My Neighbor the Sorcerer.mkv\",\n \"duration\": 1422.061,\n \"position\": 71.787,\n \"remaining\": 1350.274,\n \"media-title\": \"Dorohedoro 02 - In the Bag | Eat Quietly During Meals | My Neighbor the Sorcerer\",\n \"playlist\": [\n {\n \"index\": 0,\n \"id\": 1,\n \"filePath\": \"V:\\\\anime\\\\Vivy - Fluorite Eye's Song S01 1080p\\\\Vivy - Fluorite Eye's Song - E02.mkv\",\n \"filename\": \"Vivy - Fluorite Eye's Song - E02.mkv\"\n },\n {\n \"index\": 1,\n \"id\": 2,\n \"filePath\": \"V:\\\\anime\\\\Dorohedoro S01+OVA (BD 1080p)\\\\Dorohedoro 02 - In the Bag - Eat Quietly During Meals - My Neighbor the Sorcerer.mkv\",\n \"current\": true,\n \"filename\": \"Dorohedoro 02 - In the Bag - Eat Quietly During Meals - My Neighbor the Sorcerer.mkv\"\n }\n ],\n \"chapter\": 0,\n \"chapter-list\": [\n {\n \"title\": \"Intro\",\n \"time\": 0\n },\n {\n \"title\": \"OP\",\n \"time\": 68.986\n },\n {\n \"title\": \"Part A\",\n \"time\": 159.034\n },\n {\n \"title\": \"Part B\",\n \"time\": 589.089\n },\n {\n \"title\": \"ED\",\n \"time\": 1316.023\n },\n {\n \"title\": \"Preview\",\n \"time\": 1406.03\n }\n ],\n \"volume\": 100,\n \"fullscreen\": false,\n \"speed\": 1,\n \"sub-delay\": 0,\n \"sub-visibility\": true,\n \"track-list\": [\n {\n \"index\": 0,\n \"id\": 1,\n \"type\": \"video\",\n \"selected\": true,\n \"codec\": \"hevc\",\n \"demux-w\": 1920,\n \"demux-h\": 1080\n },\n {\n \"index\": 1,\n \"id\": 1,\n \"type\": \"audio\",\n \"selected\": true,\n \"codec\": \"opus\",\n \"demux-channel-count\": 2,\n \"demux-channels\": \"unknown2\",\n \"demux-samplerate\": 48000,\n \"lang\": \"jpn\"\n },\n {\n \"index\": 2,\n \"id\": 2,\n \"type\": \"audio\",\n \"selected\": false,\n \"codec\": \"opus\",\n \"demux-channel-count\": 2,\n \"demux-channels\": \"unknown2\",\n \"demux-samplerate\": 48000,\n \"lang\": \"eng\"\n },\n {\n \"index\": 3,\n \"id\": 1,\n \"type\": \"sub\",\n \"selected\": true,\n \"codec\": \"subrip\",\n \"lang\": \"hun\"\n },\n {\n \"index\": 4,\n \"id\": 2,\n \"type\": \"sub\",\n \"selected\": false,\n \"codec\": \"ass\",\n \"lang\": \"eng\"\n },\n {\n \"index\": 5,\n \"id\": 3,\n \"type\": \"sub\",\n \"selected\": false,\n \"codec\": \"ass\",\n \"lang\": \"eng\"\n }\n ],\n \"audio-delay\": 0,\n \"sub-font-size\": 55,\n \"sub-ass-override\": true,\n \"metadata\": {}\n}"
174 | }
175 | ]
176 | }
177 | ]
178 | },
179 | {
180 | "name": "Media controls",
181 | "item": [
182 | {
183 | "name": "Play-Pause",
184 | "request": {
185 | "method": "POST",
186 | "header": [],
187 | "url": {
188 | "raw": "{{baseURL}}/controls/play-pause",
189 | "host": [
190 | "{{baseURL}}"
191 | ],
192 | "path": [
193 | "controls",
194 | "play-pause"
195 | ]
196 | }
197 | },
198 | "response": []
199 | },
200 | {
201 | "name": "Play",
202 | "request": {
203 | "method": "POST",
204 | "header": [],
205 | "url": {
206 | "raw": "{{baseURL}}/controls/play",
207 | "host": [
208 | "{{baseURL}}"
209 | ],
210 | "path": [
211 | "controls",
212 | "play"
213 | ]
214 | }
215 | },
216 | "response": []
217 | },
218 | {
219 | "name": "Pause",
220 | "request": {
221 | "method": "POST",
222 | "header": [],
223 | "url": {
224 | "raw": "{{baseURL}}/controls/pause",
225 | "host": [
226 | "{{baseURL}}"
227 | ],
228 | "path": [
229 | "controls",
230 | "pause"
231 | ]
232 | }
233 | },
234 | "response": []
235 | },
236 | {
237 | "name": "Stop",
238 | "request": {
239 | "method": "POST",
240 | "header": [],
241 | "url": {
242 | "raw": "{{baseURL}}/controls/stop",
243 | "host": [
244 | "{{baseURL}}"
245 | ],
246 | "path": [
247 | "controls",
248 | "stop"
249 | ]
250 | }
251 | },
252 | "response": []
253 | },
254 | {
255 | "name": "Cycle fullscreen",
256 | "request": {
257 | "method": "POST",
258 | "header": [],
259 | "url": {
260 | "raw": "{{baseURL}}/controls/fullscreen",
261 | "host": [
262 | "{{baseURL}}"
263 | ],
264 | "path": [
265 | "controls",
266 | "fullscreen"
267 | ]
268 | }
269 | },
270 | "response": []
271 | },
272 | {
273 | "name": "Cycle mute",
274 | "request": {
275 | "method": "POST",
276 | "header": [],
277 | "url": {
278 | "raw": "{{baseURL}}/controls/mute",
279 | "host": [
280 | "{{baseURL}}"
281 | ],
282 | "path": [
283 | "controls",
284 | "mute"
285 | ]
286 | }
287 | },
288 | "response": []
289 | },
290 | {
291 | "name": "Set volume to 50%",
292 | "request": {
293 | "method": "POST",
294 | "header": [],
295 | "url": {
296 | "raw": "{{baseURL}}/controls/volume/50",
297 | "host": [
298 | "{{baseURL}}"
299 | ],
300 | "path": [
301 | "controls",
302 | "volume",
303 | "50"
304 | ]
305 | }
306 | },
307 | "response": []
308 | },
309 | {
310 | "name": "Seek",
311 | "request": {
312 | "method": "POST",
313 | "header": [],
314 | "body": {
315 | "mode": "raw",
316 | "raw": "{\r\n \"target\": 50.0,\r\n \"flag\": \"relative\"\r\n}",
317 | "options": {
318 | "raw": {
319 | "language": "json"
320 | }
321 | }
322 | },
323 | "url": {
324 | "raw": "{{baseURL}}/controls/seek",
325 | "host": [
326 | "{{baseURL}}"
327 | ],
328 | "path": [
329 | "controls",
330 | "seek"
331 | ]
332 | }
333 | },
334 | "response": [
335 | {
336 | "name": "Seek relative pos",
337 | "originalRequest": {
338 | "method": "POST",
339 | "header": [],
340 | "body": {
341 | "mode": "raw",
342 | "raw": "{\r\n \"target\": 50.0,\r\n \"flag\": \"relative\"\r\n}",
343 | "options": {
344 | "raw": {
345 | "language": "json"
346 | }
347 | }
348 | },
349 | "url": {
350 | "raw": "{{baseURL}}/controls/seek",
351 | "host": [
352 | "{{baseURL}}"
353 | ],
354 | "path": [
355 | "controls",
356 | "seek"
357 | ]
358 | }
359 | },
360 | "status": "OK",
361 | "code": 200,
362 | "_postman_previewlanguage": "json",
363 | "header": [
364 | {
365 | "key": "X-Powered-By",
366 | "value": "Express"
367 | },
368 | {
369 | "key": "Access-Control-Allow-Origin",
370 | "value": "*"
371 | },
372 | {
373 | "key": "Content-Type",
374 | "value": "application/json; charset=utf-8"
375 | },
376 | {
377 | "key": "Content-Length",
378 | "value": "21"
379 | },
380 | {
381 | "key": "ETag",
382 | "value": "W/\"15-ga8EF/lp+ThIsc8w/OHbk4hPrME\""
383 | },
384 | {
385 | "key": "Date",
386 | "value": "Sat, 06 Nov 2021 08:38:51 GMT"
387 | },
388 | {
389 | "key": "Connection",
390 | "value": "keep-alive"
391 | },
392 | {
393 | "key": "Keep-Alive",
394 | "value": "timeout=5"
395 | }
396 | ],
397 | "cookie": [],
398 | "body": "{\n \"message\": \"success\"\n}"
399 | },
400 | {
401 | "name": "Seek absolute pos",
402 | "originalRequest": {
403 | "method": "POST",
404 | "header": [],
405 | "body": {
406 | "mode": "raw",
407 | "raw": "{\r\n \"target\": 30.0,\r\n \"flag\": \"absolute\"\r\n}",
408 | "options": {
409 | "raw": {
410 | "language": "json"
411 | }
412 | }
413 | },
414 | "url": {
415 | "raw": "{{baseURL}}/controls/seek",
416 | "host": [
417 | "{{baseURL}}"
418 | ],
419 | "path": [
420 | "controls",
421 | "seek"
422 | ]
423 | }
424 | },
425 | "status": "OK",
426 | "code": 200,
427 | "_postman_previewlanguage": "json",
428 | "header": [
429 | {
430 | "key": "X-Powered-By",
431 | "value": "Express"
432 | },
433 | {
434 | "key": "Access-Control-Allow-Origin",
435 | "value": "*"
436 | },
437 | {
438 | "key": "Content-Type",
439 | "value": "application/json; charset=utf-8"
440 | },
441 | {
442 | "key": "Content-Length",
443 | "value": "21"
444 | },
445 | {
446 | "key": "ETag",
447 | "value": "W/\"15-ga8EF/lp+ThIsc8w/OHbk4hPrME\""
448 | },
449 | {
450 | "key": "Date",
451 | "value": "Sat, 06 Nov 2021 08:39:38 GMT"
452 | },
453 | {
454 | "key": "Connection",
455 | "value": "keep-alive"
456 | },
457 | {
458 | "key": "Keep-Alive",
459 | "value": "timeout=5"
460 | }
461 | ],
462 | "cookie": [],
463 | "body": "{\n \"message\": \"success\"\n}"
464 | }
465 | ]
466 | }
467 | ]
468 | },
469 | {
470 | "name": "Playlist",
471 | "item": [
472 | {
473 | "name": "Get items",
474 | "request": {
475 | "method": "GET",
476 | "header": [],
477 | "url": {
478 | "raw": "{{baseURL}}/playlist",
479 | "host": [
480 | "{{baseURL}}"
481 | ],
482 | "path": [
483 | "playlist"
484 | ]
485 | }
486 | },
487 | "response": [
488 | {
489 | "name": "Get items",
490 | "originalRequest": {
491 | "method": "GET",
492 | "header": [],
493 | "url": {
494 | "raw": "{{baseURL}}/playlist",
495 | "host": [
496 | "{{baseURL}}"
497 | ],
498 | "path": [
499 | "playlist"
500 | ]
501 | }
502 | },
503 | "status": "OK",
504 | "code": 200,
505 | "_postman_previewlanguage": "json",
506 | "header": [
507 | {
508 | "key": "X-Powered-By",
509 | "value": "Express"
510 | },
511 | {
512 | "key": "Access-Control-Allow-Origin",
513 | "value": "*"
514 | },
515 | {
516 | "key": "Content-Type",
517 | "value": "application/json; charset=utf-8"
518 | },
519 | {
520 | "key": "Content-Length",
521 | "value": "550"
522 | },
523 | {
524 | "key": "ETag",
525 | "value": "W/\"226-bTB/dVgK9PfqTZO93rsmWG+eGiI\""
526 | },
527 | {
528 | "key": "Date",
529 | "value": "Sat, 06 Nov 2021 08:42:27 GMT"
530 | },
531 | {
532 | "key": "Connection",
533 | "value": "keep-alive"
534 | },
535 | {
536 | "key": "Keep-Alive",
537 | "value": "timeout=5"
538 | }
539 | ],
540 | "cookie": [],
541 | "body": "[\n {\n \"index\": 0,\n \"id\": 2,\n \"filePath\": \"V:\\\\anime\\\\Dorohedoro S01+OVA (BD 1080p)\\\\Dorohedoro 02 - In the Bag - Eat Quietly During Meals - My Neighbor the Sorcerer.mkv\",\n \"current\": true,\n \"filename\": \"Dorohedoro 02 - In the Bag - Eat Quietly During Meals - My Neighbor the Sorcerer.mkv\"\n },\n {\n \"index\": 1,\n \"id\": 3,\n \"filePath\": \"V:\\\\anime\\\\Dorohedoro S01+OVA (BD 1080p)\\\\Dorohedoro 03 - Night of the Dead ~ Duel! ~ In Front of the Main Department Store.mkv\",\n \"filename\": \"Dorohedoro 03 - Night of the Dead ~ Duel! ~ In Front of the Main Department Store.mkv\"\n }\n]"
542 | }
543 | ]
544 | },
545 | {
546 | "name": "Add playlist item",
547 | "request": {
548 | "method": "POST",
549 | "header": [],
550 | "body": {
551 | "mode": "raw",
552 | "raw": "{\r\n \"filename\": \"https://www.youtube.com/watch?v=LXb3EKWsInQ\",\r\n \"flag\": \"replace\"\r\n}",
553 | "options": {
554 | "raw": {
555 | "language": "json"
556 | }
557 | }
558 | },
559 | "url": {
560 | "raw": "{{baseURL}}/playlist",
561 | "host": [
562 | "{{baseURL}}"
563 | ],
564 | "path": [
565 | "playlist"
566 | ]
567 | }
568 | },
569 | "response": [
570 | {
571 | "name": "Append to playlist",
572 | "originalRequest": {
573 | "method": "POST",
574 | "header": [],
575 | "body": {
576 | "mode": "raw",
577 | "raw": "{\r\n \"filename\": \"V:\\\\anime\\\\Vivy - Fluorite Eye's Song S01 1080p\\\\Vivy - Fluorite Eye's Song - E02.mkv\",\r\n \"flag\": \"append-play\"\r\n}",
578 | "options": {
579 | "raw": {
580 | "language": "json"
581 | }
582 | }
583 | },
584 | "url": {
585 | "raw": "{{baseURL}}/playlist",
586 | "host": [
587 | "{{baseURL}}"
588 | ],
589 | "path": [
590 | "playlist"
591 | ]
592 | }
593 | },
594 | "status": "OK",
595 | "code": 200,
596 | "_postman_previewlanguage": "json",
597 | "header": [
598 | {
599 | "key": "X-Powered-By",
600 | "value": "Express"
601 | },
602 | {
603 | "key": "Access-Control-Allow-Origin",
604 | "value": "*"
605 | },
606 | {
607 | "key": "Content-Type",
608 | "value": "application/json; charset=utf-8"
609 | },
610 | {
611 | "key": "Content-Length",
612 | "value": "21"
613 | },
614 | {
615 | "key": "ETag",
616 | "value": "W/\"15-ga8EF/lp+ThIsc8w/OHbk4hPrME\""
617 | },
618 | {
619 | "key": "Date",
620 | "value": "Sat, 06 Nov 2021 08:47:21 GMT"
621 | },
622 | {
623 | "key": "Connection",
624 | "value": "keep-alive"
625 | },
626 | {
627 | "key": "Keep-Alive",
628 | "value": "timeout=5"
629 | }
630 | ],
631 | "cookie": [],
632 | "body": "{\n \"message\": \"success\"\n}"
633 | },
634 | {
635 | "name": "Replace playlist",
636 | "originalRequest": {
637 | "method": "POST",
638 | "header": [],
639 | "body": {
640 | "mode": "raw",
641 | "raw": "{\r\n \"filename\": \"V:\\\\anime\\\\Vivy - Fluorite Eye's Song S01 1080p\\\\Vivy - Fluorite Eye's Song - E02.mkv\",\r\n \"flag\": \"replace\"\r\n}",
642 | "options": {
643 | "raw": {
644 | "language": "json"
645 | }
646 | }
647 | },
648 | "url": {
649 | "raw": "{{baseURL}}/playlist",
650 | "host": [
651 | "{{baseURL}}"
652 | ],
653 | "path": [
654 | "playlist"
655 | ]
656 | }
657 | },
658 | "status": "OK",
659 | "code": 200,
660 | "_postman_previewlanguage": "json",
661 | "header": [
662 | {
663 | "key": "X-Powered-By",
664 | "value": "Express"
665 | },
666 | {
667 | "key": "Access-Control-Allow-Origin",
668 | "value": "*"
669 | },
670 | {
671 | "key": "Content-Type",
672 | "value": "application/json; charset=utf-8"
673 | },
674 | {
675 | "key": "Content-Length",
676 | "value": "21"
677 | },
678 | {
679 | "key": "ETag",
680 | "value": "W/\"15-ga8EF/lp+ThIsc8w/OHbk4hPrME\""
681 | },
682 | {
683 | "key": "Date",
684 | "value": "Sat, 06 Nov 2021 08:48:13 GMT"
685 | },
686 | {
687 | "key": "Connection",
688 | "value": "keep-alive"
689 | },
690 | {
691 | "key": "Keep-Alive",
692 | "value": "timeout=5"
693 | }
694 | ],
695 | "cookie": [],
696 | "body": "{\n \"message\": \"success\"\n}"
697 | },
698 | {
699 | "name": "Play Youtube video",
700 | "originalRequest": {
701 | "method": "POST",
702 | "header": [],
703 | "body": {
704 | "mode": "raw",
705 | "raw": "{\r\n \"filename\": \"https://www.youtube.com/watch?v=LXb3EKWsInQ\",\r\n \"flag\": \"replace\"\r\n}",
706 | "options": {
707 | "raw": {
708 | "language": "json"
709 | }
710 | }
711 | },
712 | "url": {
713 | "raw": "{{baseURL}}/playlist",
714 | "host": [
715 | "{{baseURL}}"
716 | ],
717 | "path": [
718 | "playlist"
719 | ]
720 | }
721 | },
722 | "status": "OK",
723 | "code": 200,
724 | "_postman_previewlanguage": "json",
725 | "header": [
726 | {
727 | "key": "X-Powered-By",
728 | "value": "Express"
729 | },
730 | {
731 | "key": "Access-Control-Allow-Origin",
732 | "value": "*"
733 | },
734 | {
735 | "key": "Content-Type",
736 | "value": "application/json; charset=utf-8"
737 | },
738 | {
739 | "key": "Content-Length",
740 | "value": "21"
741 | },
742 | {
743 | "key": "ETag",
744 | "value": "W/\"15-ga8EF/lp+ThIsc8w/OHbk4hPrME\""
745 | },
746 | {
747 | "key": "Date",
748 | "value": "Sat, 06 Nov 2021 08:49:48 GMT"
749 | },
750 | {
751 | "key": "Connection",
752 | "value": "keep-alive"
753 | },
754 | {
755 | "key": "Keep-Alive",
756 | "value": "timeout=5"
757 | }
758 | ],
759 | "cookie": [],
760 | "body": "{\n \"message\": \"success\"\n}"
761 | },
762 | {
763 | "name": "Replace playlist and seek file to 60 seconds",
764 | "originalRequest": {
765 | "method": "POST",
766 | "header": [],
767 | "body": {
768 | "mode": "raw",
769 | "raw": "{\r\n \"filename\": \"V:\\\\anime\\\\Vivy - Fluorite Eye's Song S01 1080p\\\\Vivy - Fluorite Eye's Song - E02.mkv\",\r\n \"flag\": \"replace\",\r\n \"seekTo\": 60.0\r\n}",
770 | "options": {
771 | "raw": {
772 | "language": "json"
773 | }
774 | }
775 | },
776 | "url": {
777 | "raw": "{{baseURL}}/playlist",
778 | "host": [
779 | "{{baseURL}}"
780 | ],
781 | "path": [
782 | "playlist"
783 | ]
784 | }
785 | },
786 | "status": "OK",
787 | "code": 200,
788 | "_postman_previewlanguage": "json",
789 | "header": [
790 | {
791 | "key": "X-Powered-By",
792 | "value": "Express"
793 | },
794 | {
795 | "key": "Access-Control-Allow-Origin",
796 | "value": "*"
797 | },
798 | {
799 | "key": "Content-Type",
800 | "value": "application/json; charset=utf-8"
801 | },
802 | {
803 | "key": "Content-Length",
804 | "value": "21"
805 | },
806 | {
807 | "key": "ETag",
808 | "value": "W/\"15-ga8EF/lp+ThIsc8w/OHbk4hPrME\""
809 | },
810 | {
811 | "key": "Date",
812 | "value": "Sat, 06 Nov 2021 08:51:43 GMT"
813 | },
814 | {
815 | "key": "Connection",
816 | "value": "keep-alive"
817 | },
818 | {
819 | "key": "Keep-Alive",
820 | "value": "timeout=5"
821 | }
822 | ],
823 | "cookie": [],
824 | "body": "{\n \"message\": \"success\"\n}"
825 | }
826 | ]
827 | },
828 | {
829 | "name": "Remove first item",
830 | "request": {
831 | "method": "DELETE",
832 | "header": [],
833 | "url": {
834 | "raw": "{{baseURL}}/playlist/remove/0",
835 | "host": [
836 | "{{baseURL}}"
837 | ],
838 | "path": [
839 | "playlist",
840 | "remove",
841 | "0"
842 | ]
843 | }
844 | },
845 | "response": []
846 | },
847 | {
848 | "name": "Move item",
849 | "request": {
850 | "method": "POST",
851 | "header": [],
852 | "url": {
853 | "raw": "{{baseURL}}/playlist/move?fromIndex=0&toIndex=2",
854 | "host": [
855 | "{{baseURL}}"
856 | ],
857 | "path": [
858 | "playlist",
859 | "move"
860 | ],
861 | "query": [
862 | {
863 | "key": "fromIndex",
864 | "value": "0"
865 | },
866 | {
867 | "key": "toIndex",
868 | "value": "2"
869 | }
870 | ]
871 | }
872 | },
873 | "response": []
874 | },
875 | {
876 | "name": "Play an item",
877 | "request": {
878 | "method": "POST",
879 | "header": [],
880 | "url": {
881 | "raw": "{{baseURL}}/playlist/play/1",
882 | "host": [
883 | "{{baseURL}}"
884 | ],
885 | "path": [
886 | "playlist",
887 | "play",
888 | "1"
889 | ]
890 | }
891 | },
892 | "response": []
893 | },
894 | {
895 | "name": "Clear playlist",
896 | "request": {
897 | "method": "POST",
898 | "header": [],
899 | "url": {
900 | "raw": "{{baseURL}}/playlist/clear",
901 | "host": [
902 | "{{baseURL}}"
903 | ],
904 | "path": [
905 | "playlist",
906 | "clear"
907 | ]
908 | }
909 | },
910 | "response": []
911 | },
912 | {
913 | "name": "Shuffle",
914 | "request": {
915 | "method": "POST",
916 | "header": [],
917 | "url": {
918 | "raw": "{{baseURL}}/playlist/shuffle",
919 | "host": [
920 | "{{baseURL}}"
921 | ],
922 | "path": [
923 | "playlist",
924 | "shuffle"
925 | ]
926 | }
927 | },
928 | "response": []
929 | }
930 | ]
931 | },
932 | {
933 | "name": "Tracks",
934 | "item": [
935 | {
936 | "name": "Audio",
937 | "item": [
938 | {
939 | "name": "Cycle between tracks",
940 | "request": {
941 | "method": "POST",
942 | "header": [],
943 | "url": {
944 | "raw": "{{baseURL}}/tracks/audio/cycle",
945 | "host": [
946 | "{{baseURL}}"
947 | ],
948 | "path": [
949 | "tracks",
950 | "audio",
951 | "cycle"
952 | ]
953 | }
954 | },
955 | "response": []
956 | },
957 | {
958 | "name": "Reload track ID:2",
959 | "request": {
960 | "method": "POST",
961 | "header": [],
962 | "url": {
963 | "raw": "{{baseURL}}/tracks/audio/reload/2",
964 | "host": [
965 | "{{baseURL}}"
966 | ],
967 | "path": [
968 | "tracks",
969 | "audio",
970 | "reload",
971 | "2"
972 | ]
973 | }
974 | },
975 | "response": []
976 | },
977 | {
978 | "name": "Set audio timing",
979 | "request": {
980 | "method": "POST",
981 | "header": [],
982 | "url": {
983 | "raw": "{{baseURL}}/tracks/audio/timing/5",
984 | "host": [
985 | "{{baseURL}}"
986 | ],
987 | "path": [
988 | "tracks",
989 | "audio",
990 | "timing",
991 | "5"
992 | ]
993 | }
994 | },
995 | "response": []
996 | }
997 | ]
998 | },
999 | {
1000 | "name": "Subtitle",
1001 | "item": [
1002 | {
1003 | "name": "Reload track ID: 2",
1004 | "request": {
1005 | "method": "POST",
1006 | "header": [],
1007 | "url": {
1008 | "raw": "{{baseURL}}/tracks/sub/reload/2",
1009 | "host": [
1010 | "{{baseURL}}"
1011 | ],
1012 | "path": [
1013 | "tracks",
1014 | "sub",
1015 | "reload",
1016 | "2"
1017 | ]
1018 | }
1019 | },
1020 | "response": []
1021 | },
1022 | {
1023 | "name": "Subtitle timing",
1024 | "request": {
1025 | "method": "POST",
1026 | "header": [],
1027 | "url": {
1028 | "raw": "{{baseURL}}/tracks/sub/timing/0",
1029 | "host": [
1030 | "{{baseURL}}"
1031 | ],
1032 | "path": [
1033 | "tracks",
1034 | "sub",
1035 | "timing",
1036 | "0"
1037 | ]
1038 | }
1039 | },
1040 | "response": []
1041 | },
1042 | {
1043 | "name": "ASS Override",
1044 | "request": {
1045 | "method": "POST",
1046 | "header": [],
1047 | "url": {
1048 | "raw": "{{baseURL}}/tracks/sub/ass-override/yes",
1049 | "host": [
1050 | "{{baseURL}}"
1051 | ],
1052 | "path": [
1053 | "tracks",
1054 | "sub",
1055 | "ass-override",
1056 | "yes"
1057 | ]
1058 | }
1059 | },
1060 | "response": []
1061 | },
1062 | {
1063 | "name": "Toggle visibility",
1064 | "request": {
1065 | "method": "POST",
1066 | "header": [],
1067 | "url": {
1068 | "raw": "{{baseURL}}/tracks/sub/toggle-visibility",
1069 | "host": [
1070 | "{{baseURL}}"
1071 | ],
1072 | "path": [
1073 | "tracks",
1074 | "sub",
1075 | "toggle-visibility"
1076 | ]
1077 | }
1078 | },
1079 | "response": []
1080 | },
1081 | {
1082 | "name": "Set visibility",
1083 | "request": {
1084 | "method": "POST",
1085 | "header": [],
1086 | "url": {
1087 | "raw": "{{baseURL}}/tracks/sub/visibility/true",
1088 | "host": [
1089 | "{{baseURL}}"
1090 | ],
1091 | "path": [
1092 | "tracks",
1093 | "sub",
1094 | "visibility",
1095 | "true"
1096 | ]
1097 | }
1098 | },
1099 | "response": []
1100 | },
1101 | {
1102 | "name": "Add track",
1103 | "request": {
1104 | "method": "POST",
1105 | "header": [],
1106 | "body": {
1107 | "mode": "raw",
1108 | "raw": "{\r\n \"filename\": \"V:\\\\anime\\\\Dorohedoro S01+OVA (BD 1080p)\\\\Feliratok\\\\[Judas] Dorohedoro - 03 [1080p][HEVC x265 10bit][Multi-Subs].srt\",\r\n \"flag\": \"select\"\r\n}",
1109 | "options": {
1110 | "raw": {
1111 | "language": "json"
1112 | }
1113 | }
1114 | },
1115 | "url": {
1116 | "raw": "{{baseURL}}/tracks/sub/add",
1117 | "host": [
1118 | "{{baseURL}}"
1119 | ],
1120 | "path": [
1121 | "tracks",
1122 | "sub",
1123 | "add"
1124 | ]
1125 | }
1126 | },
1127 | "response": []
1128 | }
1129 | ]
1130 | },
1131 | {
1132 | "name": "Get tracks",
1133 | "request": {
1134 | "method": "GET",
1135 | "header": [],
1136 | "url": {
1137 | "raw": "{{baseURL}}/tracks",
1138 | "host": [
1139 | "{{baseURL}}"
1140 | ],
1141 | "path": [
1142 | "tracks"
1143 | ]
1144 | }
1145 | },
1146 | "response": [
1147 | {
1148 | "name": "Get tracks",
1149 | "originalRequest": {
1150 | "method": "GET",
1151 | "header": [],
1152 | "url": {
1153 | "raw": "{{baseURL}}/tracks",
1154 | "host": [
1155 | "{{baseURL}}"
1156 | ],
1157 | "path": [
1158 | "tracks"
1159 | ]
1160 | }
1161 | },
1162 | "status": "OK",
1163 | "code": 200,
1164 | "_postman_previewlanguage": "json",
1165 | "header": [
1166 | {
1167 | "key": "X-Powered-By",
1168 | "value": "Express"
1169 | },
1170 | {
1171 | "key": "Access-Control-Allow-Origin",
1172 | "value": "*"
1173 | },
1174 | {
1175 | "key": "Content-Type",
1176 | "value": "application/json; charset=utf-8"
1177 | },
1178 | {
1179 | "key": "Content-Length",
1180 | "value": "637"
1181 | },
1182 | {
1183 | "key": "ETag",
1184 | "value": "W/\"27d-suKNO6rn4DGM3+CnAnzqyYORMUU\""
1185 | },
1186 | {
1187 | "key": "Date",
1188 | "value": "Sat, 06 Nov 2021 09:06:20 GMT"
1189 | },
1190 | {
1191 | "key": "Connection",
1192 | "value": "keep-alive"
1193 | },
1194 | {
1195 | "key": "Keep-Alive",
1196 | "value": "timeout=5"
1197 | }
1198 | ],
1199 | "cookie": [],
1200 | "body": "[\n {\n \"index\": 0,\n \"id\": 1,\n \"type\": \"video\",\n \"selected\": true,\n \"codec\": \"hevc\",\n \"demux-w\": 1920,\n \"demux-h\": 1080\n },\n {\n \"index\": 1,\n \"id\": 1,\n \"type\": \"audio\",\n \"selected\": true,\n \"codec\": \"opus\",\n \"demux-channel-count\": 2,\n \"demux-channels\": \"unknown2\",\n \"demux-samplerate\": 48000,\n \"lang\": \"jpn\"\n },\n {\n \"index\": 2,\n \"id\": 2,\n \"type\": \"audio\",\n \"selected\": false,\n \"codec\": \"opus\",\n \"demux-channel-count\": 2,\n \"demux-channels\": \"unknown2\",\n \"demux-samplerate\": 48000,\n \"lang\": \"eng\"\n },\n {\n \"index\": 3,\n \"id\": 1,\n \"type\": \"sub\",\n \"selected\": true,\n \"codec\": \"subrip\",\n \"lang\": \"hun\"\n },\n {\n \"index\": 4,\n \"id\": 2,\n \"type\": \"sub\",\n \"selected\": false,\n \"codec\": \"ass\",\n \"lang\": \"eng\"\n },\n {\n \"index\": 5,\n \"id\": 3,\n \"type\": \"sub\",\n \"selected\": false,\n \"codec\": \"ass\",\n \"lang\": \"eng\"\n }\n]"
1201 | }
1202 | ]
1203 | }
1204 | ]
1205 | },
1206 | {
1207 | "name": "Filebrowser",
1208 | "item": [
1209 | {
1210 | "name": "Browse path",
1211 | "request": {
1212 | "method": "POST",
1213 | "header": [],
1214 | "body": {
1215 | "mode": "raw",
1216 | "raw": "{\r\n \"path\": \"V:\\\\anime\\\\Dorohedoro S01+OVA (BD 1080p)\"\r\n}",
1217 | "options": {
1218 | "raw": {
1219 | "language": "json"
1220 | }
1221 | }
1222 | },
1223 | "url": {
1224 | "raw": "{{baseURL}}/filebrowser/browse",
1225 | "host": [
1226 | "{{baseURL}}"
1227 | ],
1228 | "path": [
1229 | "filebrowser",
1230 | "browse"
1231 | ]
1232 | }
1233 | },
1234 | "response": [
1235 | {
1236 | "name": "Browse path",
1237 | "originalRequest": {
1238 | "method": "POST",
1239 | "header": [],
1240 | "body": {
1241 | "mode": "raw",
1242 | "raw": "{\r\n \"path\": \"V:\\\\anime\\\\Dorohedoro S01+OVA (BD 1080p)\"\r\n}",
1243 | "options": {
1244 | "raw": {
1245 | "language": "json"
1246 | }
1247 | }
1248 | },
1249 | "url": {
1250 | "raw": "{{baseURL}}/filebrowser/browse",
1251 | "host": [
1252 | "{{baseURL}}"
1253 | ],
1254 | "path": [
1255 | "filebrowser",
1256 | "browse"
1257 | ]
1258 | }
1259 | },
1260 | "status": "OK",
1261 | "code": 200,
1262 | "_postman_previewlanguage": "json",
1263 | "header": [
1264 | {
1265 | "key": "X-Powered-By",
1266 | "value": "Express"
1267 | },
1268 | {
1269 | "key": "Access-Control-Allow-Origin",
1270 | "value": "*"
1271 | },
1272 | {
1273 | "key": "Content-Type",
1274 | "value": "application/json; charset=utf-8"
1275 | },
1276 | {
1277 | "key": "Content-Length",
1278 | "value": "4366"
1279 | },
1280 | {
1281 | "key": "ETag",
1282 | "value": "W/\"110e-W2Hs55tXyLo4W7snTrwtjT/R+fU\""
1283 | },
1284 | {
1285 | "key": "Date",
1286 | "value": "Sat, 06 Nov 2021 09:32:38 GMT"
1287 | },
1288 | {
1289 | "key": "Connection",
1290 | "value": "keep-alive"
1291 | },
1292 | {
1293 | "key": "Keep-Alive",
1294 | "value": "timeout=5"
1295 | }
1296 | ],
1297 | "cookie": [],
1298 | "body": "{\n \"content\": [\n {\n \"priority\": 1,\n \"type\": \"directory\",\n \"name\": \"Feliratok\",\n \"fullPath\": \"V:\\\\anime\\\\Dorohedoro S01+OVA (BD 1080p)\\\\Feliratok\",\n \"lastModified\": \"2021-05-03T19:10:16.008Z\"\n },\n {\n \"priority\": 2,\n \"type\": \"video\",\n \"name\": \"Dorohedoro 01 - Caiman.mkv\",\n \"fullPath\": \"V:\\\\anime\\\\Dorohedoro S01+OVA (BD 1080p)\\\\Dorohedoro 01 - Caiman.mkv\",\n \"lastModified\": \"2021-05-03T19:09:21.857Z\",\n \"mediaStatus\": {\n // Media status appears only when localdb enabled!\n \"id\": 2,\n \"directory\": \"V:\\\\anime\\\\Dorohedoro S01+OVA (BD 1080p)\",\n \"file_name\": \"Dorohedoro 01 - Caiman.mkv\",\n \"current_time\": 1422.0545,\n \"finished\": 1\n }\n },\n {\n \"priority\": 2,\n \"type\": \"video\",\n \"name\": \"Dorohedoro 02 - In the Bag - Eat Quietly During Meals - My Neighbor the Sorcerer.mkv\",\n \"fullPath\": \"V:\\\\anime\\\\Dorohedoro S01+OVA (BD 1080p)\\\\Dorohedoro 02 - In the Bag - Eat Quietly During Meals - My Neighbor the Sorcerer.mkv\",\n \"lastModified\": \"2021-05-03T19:04:05.410Z\",\n \"mediaStatus\": {\n \"id\": 1,\n \"directory\": \"V:\\\\anime\\\\Dorohedoro S01+OVA (BD 1080p)\",\n \"file_name\": \"Dorohedoro 02 - In the Bag - Eat Quietly During Meals - My Neighbor the Sorcerer.mkv\",\n \"current_time\": 596.853,\n \"finished\": 0\n }\n },\n {\n \"priority\": 2,\n \"type\": \"video\",\n \"name\": \"Dorohedoro 03 - Night of the Dead ~ Duel! ~ In Front of the Main Department Store.mkv\",\n \"fullPath\": \"V:\\\\anime\\\\Dorohedoro S01+OVA (BD 1080p)\\\\Dorohedoro 03 - Night of the Dead ~ Duel! ~ In Front of the Main Department Store.mkv\",\n \"lastModified\": \"2021-05-03T19:04:26.434Z\"\n }\n ],\n \"dirname\": \"Dorohedoro S01+OVA (BD 1080p)\",\n \"prevDir\": \"V:\\\\anime\",\n \"cwd\": \"V:\\\\anime\\\\Dorohedoro S01+OVA (BD 1080p)\"\n}"
1299 | }
1300 | ]
1301 | },
1302 | {
1303 | "name": "Paths (Only interesting if mpvremote-filebrowserpaths defined)",
1304 | "request": {
1305 | "method": "GET",
1306 | "header": [],
1307 | "url": {
1308 | "raw": "{{baseURL}}/filebrowser/paths",
1309 | "host": [
1310 | "{{baseURL}}"
1311 | ],
1312 | "path": [
1313 | "filebrowser",
1314 | "paths"
1315 | ]
1316 | }
1317 | },
1318 | "response": []
1319 | },
1320 | {
1321 | "name": "Browse path index 1 (Only interesting if mpvremote-filebrowserpaths defined)",
1322 | "request": {
1323 | "method": "GET",
1324 | "header": [],
1325 | "url": {
1326 | "raw": "{{baseURL}}/filebrowser/paths/1",
1327 | "host": [
1328 | "{{baseURL}}"
1329 | ],
1330 | "path": [
1331 | "filebrowser",
1332 | "paths",
1333 | "1"
1334 | ]
1335 | }
1336 | },
1337 | "response": []
1338 | },
1339 | {
1340 | "name": "Get drives",
1341 | "request": {
1342 | "method": "GET",
1343 | "header": [],
1344 | "url": {
1345 | "raw": "{{baseURL}}/drives",
1346 | "host": [
1347 | "{{baseURL}}"
1348 | ],
1349 | "path": [
1350 | "drives"
1351 | ]
1352 | }
1353 | },
1354 | "response": [
1355 | {
1356 | "name": "Get drives (Windows)",
1357 | "originalRequest": {
1358 | "method": "GET",
1359 | "header": [],
1360 | "url": {
1361 | "raw": "{{baseURL}}/drives",
1362 | "host": [
1363 | "{{baseURL}}"
1364 | ],
1365 | "path": [
1366 | "drives"
1367 | ]
1368 | }
1369 | },
1370 | "status": "OK",
1371 | "code": 200,
1372 | "_postman_previewlanguage": "json",
1373 | "header": [
1374 | {
1375 | "key": "X-Powered-By",
1376 | "value": "Express"
1377 | },
1378 | {
1379 | "key": "Access-Control-Allow-Origin",
1380 | "value": "*"
1381 | },
1382 | {
1383 | "key": "Content-Type",
1384 | "value": "application/json; charset=utf-8"
1385 | },
1386 | {
1387 | "key": "Content-Length",
1388 | "value": "99"
1389 | },
1390 | {
1391 | "key": "ETag",
1392 | "value": "W/\"63-OKbKxXRqyUYRQu2WGQHNRCkbFbM\""
1393 | },
1394 | {
1395 | "key": "Date",
1396 | "value": "Sat, 06 Nov 2021 09:44:40 GMT"
1397 | },
1398 | {
1399 | "key": "Connection",
1400 | "value": "keep-alive"
1401 | },
1402 | {
1403 | "key": "Keep-Alive",
1404 | "value": "timeout=5"
1405 | }
1406 | ],
1407 | "cookie": [],
1408 | "body": "[\n {\n \"path\": \"C:\"\n },\n {\n \"path\": \"D:\"\n },\n {\n \"path\": \"E:\"\n },\n {\n \"path\": \"V:\"\n },\n {\n \"path\": \"W:\"\n },\n {\n \"path\": \"X:\"\n },\n {\n \"path\": \"Y:\"\n }\n]"
1409 | },
1410 | {
1411 | "name": "Get drives (Linux)",
1412 | "originalRequest": {
1413 | "method": "GET",
1414 | "header": [],
1415 | "url": {
1416 | "raw": "{{baseURL}}/drives",
1417 | "host": [
1418 | "{{baseURL}}"
1419 | ],
1420 | "path": [
1421 | "drives"
1422 | ]
1423 | }
1424 | },
1425 | "status": "OK",
1426 | "code": 200,
1427 | "_postman_previewlanguage": "json",
1428 | "header": [
1429 | {
1430 | "key": "X-Powered-By",
1431 | "value": "Express"
1432 | },
1433 | {
1434 | "key": "Access-Control-Allow-Origin",
1435 | "value": "*"
1436 | },
1437 | {
1438 | "key": "Content-Type",
1439 | "value": "application/json; charset=utf-8"
1440 | },
1441 | {
1442 | "key": "Content-Length",
1443 | "value": "160"
1444 | },
1445 | {
1446 | "key": "ETag",
1447 | "value": "W/\"a0-1FoXGAYfoynPfQoMfagnvt9tpvs\""
1448 | },
1449 | {
1450 | "key": "Date",
1451 | "value": "Sat, 06 Nov 2021 13:12:14 GMT"
1452 | },
1453 | {
1454 | "key": "Connection",
1455 | "value": "keep-alive"
1456 | },
1457 | {
1458 | "key": "Keep-Alive",
1459 | "value": "timeout=5"
1460 | }
1461 | ],
1462 | "cookie": [],
1463 | "body": "[\n {\n \"path\": \"/dev\"\n },\n {\n \"path\": \"/run\"\n },\n {\n \"path\": \"/\"\n },\n {\n \"path\": \"/dev/shm\"\n },\n {\n \"path\": \"/run/lock\"\n },\n {\n \"path\": \"/sys/fs/cgroup\"\n },\n {\n \"path\": \"/boot/efi\"\n },\n {\n \"path\": \"/run/user/1000\"\n }\n]"
1464 | }
1465 | ]
1466 | }
1467 | ]
1468 | },
1469 | {
1470 | "name": "Collections",
1471 | "item": [
1472 | {
1473 | "name": "Entries",
1474 | "item": [
1475 | {
1476 | "name": "Delete collection entry",
1477 | "request": {
1478 | "method": "DELETE",
1479 | "header": [],
1480 | "url": {
1481 | "raw": "{{baseURL}}/collections/1",
1482 | "host": [
1483 | "{{baseURL}}"
1484 | ],
1485 | "path": [
1486 | "collections",
1487 | "1"
1488 | ]
1489 | }
1490 | },
1491 | "response": []
1492 | },
1493 | {
1494 | "name": "Add collection entry",
1495 | "request": {
1496 | "method": "POST",
1497 | "header": [],
1498 | "body": {
1499 | "mode": "raw",
1500 | "raw": "{\r\n \"path\": \"W:\\\\anime\"\r\n}",
1501 | "options": {
1502 | "raw": {
1503 | "language": "json"
1504 | }
1505 | }
1506 | },
1507 | "url": {
1508 | "raw": "{{baseURL}}/collections/1/entries",
1509 | "host": [
1510 | "{{baseURL}}"
1511 | ],
1512 | "path": [
1513 | "collections",
1514 | "1",
1515 | "entries"
1516 | ]
1517 | }
1518 | },
1519 | "response": [
1520 | {
1521 | "name": "Collection not exists",
1522 | "originalRequest": {
1523 | "method": "POST",
1524 | "header": [],
1525 | "body": {
1526 | "mode": "raw",
1527 | "raw": "{\r\n \"path\": \"V:\\\\torrent\\\\anime\\\\\"\r\n}",
1528 | "options": {
1529 | "raw": {
1530 | "language": "json"
1531 | }
1532 | }
1533 | },
1534 | "url": {
1535 | "raw": "{{baseURL}}/collections/1/entries",
1536 | "host": [
1537 | "{{baseURL}}"
1538 | ],
1539 | "path": [
1540 | "collections",
1541 | "1",
1542 | "entries"
1543 | ]
1544 | }
1545 | },
1546 | "status": "Internal Server Error",
1547 | "code": 500,
1548 | "_postman_previewlanguage": "json",
1549 | "header": [
1550 | {
1551 | "key": "X-Powered-By",
1552 | "value": "Express"
1553 | },
1554 | {
1555 | "key": "Access-Control-Allow-Origin",
1556 | "value": "*"
1557 | },
1558 | {
1559 | "key": "Content-Type",
1560 | "value": "application/json; charset=utf-8"
1561 | },
1562 | {
1563 | "key": "Content-Length",
1564 | "value": "49"
1565 | },
1566 | {
1567 | "key": "ETag",
1568 | "value": "W/\"31-oc9di03fr86OECBCbVUILmxLPiA\""
1569 | },
1570 | {
1571 | "key": "Date",
1572 | "value": "Sat, 06 Nov 2021 09:54:43 GMT"
1573 | },
1574 | {
1575 | "key": "Connection",
1576 | "value": "keep-alive"
1577 | },
1578 | {
1579 | "key": "Keep-Alive",
1580 | "value": "timeout=5"
1581 | }
1582 | ],
1583 | "cookie": [],
1584 | "body": "{\n \"error\": {\n \"errno\": 19,\n \"code\": \"SQLITE_CONSTRAINT\"\n }\n}"
1585 | },
1586 | {
1587 | "name": "Add collection entry",
1588 | "originalRequest": {
1589 | "method": "POST",
1590 | "header": [],
1591 | "body": {
1592 | "mode": "raw",
1593 | "raw": "{\r\n \"path\": \"W:\\\\anime\"\r\n}",
1594 | "options": {
1595 | "raw": {
1596 | "language": "json"
1597 | }
1598 | }
1599 | },
1600 | "url": {
1601 | "raw": "{{baseURL}}/collections/1/entries",
1602 | "host": [
1603 | "{{baseURL}}"
1604 | ],
1605 | "path": [
1606 | "collections",
1607 | "1",
1608 | "entries"
1609 | ]
1610 | }
1611 | },
1612 | "status": "OK",
1613 | "code": 200,
1614 | "_postman_previewlanguage": "json",
1615 | "header": [
1616 | {
1617 | "key": "X-Powered-By",
1618 | "value": "Express"
1619 | },
1620 | {
1621 | "key": "Access-Control-Allow-Origin",
1622 | "value": "*"
1623 | },
1624 | {
1625 | "key": "Content-Type",
1626 | "value": "application/json; charset=utf-8"
1627 | },
1628 | {
1629 | "key": "Content-Length",
1630 | "value": "56"
1631 | },
1632 | {
1633 | "key": "ETag",
1634 | "value": "W/\"38-KvFbZUM2MBsM16wAq8cMIRAvf9c\""
1635 | },
1636 | {
1637 | "key": "Date",
1638 | "value": "Sat, 06 Nov 2021 09:55:58 GMT"
1639 | },
1640 | {
1641 | "key": "Connection",
1642 | "value": "keep-alive"
1643 | },
1644 | {
1645 | "key": "Keep-Alive",
1646 | "value": "timeout=5"
1647 | }
1648 | ],
1649 | "cookie": [],
1650 | "body": "{\n \"id\": 2,\n \"collection_id\": 1,\n \"path\": \"W:\\\\anime\\\\\"\n}"
1651 | }
1652 | ]
1653 | }
1654 | ]
1655 | },
1656 | {
1657 | "name": "Get collections",
1658 | "request": {
1659 | "method": "GET",
1660 | "header": [],
1661 | "url": {
1662 | "raw": "{{baseURL}}/collections",
1663 | "host": [
1664 | "{{baseURL}}"
1665 | ],
1666 | "path": [
1667 | "collections"
1668 | ]
1669 | }
1670 | },
1671 | "response": []
1672 | },
1673 | {
1674 | "name": "Add collection",
1675 | "request": {
1676 | "method": "POST",
1677 | "header": [],
1678 | "body": {
1679 | "mode": "raw",
1680 | "raw": "{\r\n \"name\": \"Anime\",\r\n \"type\": 2,\r\n \"paths\": [\r\n {\r\n \"path\": \"V:\\\\anime\"\r\n },\r\n {\r\n \"path\": \"W:\\\\anime\"\r\n }\r\n \r\n ]\r\n}",
1681 | "options": {
1682 | "raw": {
1683 | "language": "json"
1684 | }
1685 | }
1686 | },
1687 | "url": {
1688 | "raw": "{{baseURL}}/collections",
1689 | "host": [
1690 | "{{baseURL}}"
1691 | ],
1692 | "path": [
1693 | "collections"
1694 | ]
1695 | }
1696 | },
1697 | "response": [
1698 | {
1699 | "name": "Add collection",
1700 | "originalRequest": {
1701 | "method": "POST",
1702 | "header": [],
1703 | "body": {
1704 | "mode": "raw",
1705 | "raw": "{\r\n \"name\": \"Anime\",\r\n \"type\": 2,\r\n \"paths\": [\r\n {\r\n \"path\": \"V:\\\\anime\"\r\n },\r\n {\r\n \"path\": \"W:\\\\anime\"\r\n }\r\n \r\n ]\r\n}",
1706 | "options": {
1707 | "raw": {
1708 | "language": "json"
1709 | }
1710 | }
1711 | },
1712 | "url": {
1713 | "raw": "{{baseURL}}/collections",
1714 | "host": [
1715 | "{{baseURL}}"
1716 | ],
1717 | "path": [
1718 | "collections"
1719 | ]
1720 | }
1721 | },
1722 | "status": "OK",
1723 | "code": 200,
1724 | "_postman_previewlanguage": "json",
1725 | "header": [
1726 | {
1727 | "key": "X-Powered-By",
1728 | "value": "Express"
1729 | },
1730 | {
1731 | "key": "Access-Control-Allow-Origin",
1732 | "value": "*"
1733 | },
1734 | {
1735 | "key": "Content-Type",
1736 | "value": "application/json; charset=utf-8"
1737 | },
1738 | {
1739 | "key": "Content-Length",
1740 | "value": "43"
1741 | },
1742 | {
1743 | "key": "ETag",
1744 | "value": "W/\"2b-tO5RPbhP0CPR6TPKxgW7R4AeumY\""
1745 | },
1746 | {
1747 | "key": "Date",
1748 | "value": "Sat, 06 Nov 2021 09:47:54 GMT"
1749 | },
1750 | {
1751 | "key": "Connection",
1752 | "value": "keep-alive"
1753 | },
1754 | {
1755 | "key": "Keep-Alive",
1756 | "value": "timeout=5"
1757 | }
1758 | ],
1759 | "cookie": [],
1760 | "body": "{\n \"id\": 1,\n \"name\": \"Anime\",\n \"type\": 2,\n \"paths\": []\n}"
1761 | }
1762 | ]
1763 | },
1764 | {
1765 | "name": "Update collection",
1766 | "request": {
1767 | "method": "PATCH",
1768 | "header": [],
1769 | "url": null
1770 | },
1771 | "response": [
1772 | {
1773 | "name": "Update collection",
1774 | "originalRequest": {
1775 | "method": "PATCH",
1776 | "header": [],
1777 | "body": {
1778 | "mode": "raw",
1779 | "raw": "{\r\n \"name\": \"Anime ja nai\",\r\n \"type\": 2,\r\n \"paths\": [\r\n {\r\n \"path\": \"/home/usr/media3\"\r\n },\r\n {\r\n \"id\": 2,\r\n \"path\": \"/home/usr/media2_other\"\r\n }\r\n ]\r\n}",
1780 | "options": {
1781 | "raw": {
1782 | "language": "json"
1783 | }
1784 | }
1785 | },
1786 | "url": {
1787 | "raw": "{{baseURL}}/collections/1",
1788 | "host": [
1789 | "{{baseURL}}"
1790 | ],
1791 | "path": [
1792 | "collections",
1793 | "1"
1794 | ]
1795 | }
1796 | },
1797 | "status": "OK",
1798 | "code": 200,
1799 | "_postman_previewlanguage": "json",
1800 | "header": [
1801 | {
1802 | "key": "X-Powered-By",
1803 | "value": "Express"
1804 | },
1805 | {
1806 | "key": "Access-Control-Allow-Origin",
1807 | "value": "*"
1808 | },
1809 | {
1810 | "key": "Content-Type",
1811 | "value": "application/json; charset=utf-8"
1812 | },
1813 | {
1814 | "key": "Content-Length",
1815 | "value": "216"
1816 | },
1817 | {
1818 | "key": "ETag",
1819 | "value": "W/\"d8-Or2otxB/wzQXXaLZEiK3M+vRhj4\""
1820 | },
1821 | {
1822 | "key": "Date",
1823 | "value": "Sat, 06 Nov 2021 09:50:09 GMT"
1824 | },
1825 | {
1826 | "key": "Connection",
1827 | "value": "keep-alive"
1828 | },
1829 | {
1830 | "key": "Keep-Alive",
1831 | "value": "timeout=5"
1832 | }
1833 | ],
1834 | "cookie": [],
1835 | "body": "{\n \"id\": 1,\n \"name\": \"Anime ja nai\",\n \"type\": 2,\n \"paths\": [\n {\n \"id\": 1,\n \"collection_id\": 1,\n \"path\": \"V:\\\\anime\"\n },\n {\n \"id\": 2,\n \"collection_id\": 1,\n \"path\": \"/home/usr/media2_other\"\n },\n {\n \"id\": 3,\n \"collection_id\": 1,\n \"path\": \"/home/usr/media3\"\n }\n ]\n}"
1836 | }
1837 | ]
1838 | },
1839 | {
1840 | "name": "Delete collection",
1841 | "request": {
1842 | "method": "DELETE",
1843 | "header": [],
1844 | "url": {
1845 | "raw": "{{baseURL}}/collections/1",
1846 | "host": [
1847 | "{{baseURL}}"
1848 | ],
1849 | "path": [
1850 | "collections",
1851 | "1"
1852 | ]
1853 | }
1854 | },
1855 | "response": []
1856 | }
1857 | ]
1858 | }
1859 | ]
1860 | }
--------------------------------------------------------------------------------
/postman/MPV Remote.postman_environment.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "bea6c28d-fa74-402a-8e31-4d9224774717",
3 | "name": "MPV Remote",
4 | "values": [
5 | {
6 | "key": "baseURL",
7 | "value": "http://localhost:8000/api/v1",
8 | "enabled": true
9 | }
10 | ],
11 | "_postman_variable_scope": "environment",
12 | "_postman_exported_at": "2021-11-06T09:58:09.228Z",
13 | "_postman_exported_using": "Postman/9.1.2"
14 | }
--------------------------------------------------------------------------------
/remoteServer.js:
--------------------------------------------------------------------------------
1 | const os = require("os");
2 | const process = require("process");
3 | const fs = require("fs");
4 | const fs_async = require("fs").promises;
5 | const path = require("path");
6 | const URL = require("url").URL;
7 | const exec = require("child_process").exec;
8 |
9 | const express = require("express");
10 | const cors = require("cors");
11 | const mpvAPI = require("node-mpv");
12 |
13 | const yargs = require("yargs");
14 | const portfinder = require("portfinder");
15 |
16 | const WIN_SHUTDOWN_COMMAND = "shutdown /s /t 1";
17 | const WIN_REBOOT_COMMAND = "shutdown /r /t 1";
18 | const UNIX_SHUTDOWN_COMMAND = "/usr/sbin/shutdown now";
19 | const UNIX_REBOOT_COMMAND = "/usr/sbin/reboot";
20 |
21 | const { initDB } = require("./crud");
22 |
23 | const tempdir = process.env.TEMP || process.env.TMP || "/tmp"; // Temp dir
24 | const FILE_LOCAL_OPTIONS_PATH = path.join(tempdir, "file-local-options.txt");
25 | const filebrowser = require("./filebrowser");
26 | const collections = require("./collections");
27 | const { detectFileType } = require("./filebrowser");
28 | const { loadSettings, settings, CORSOPTIONS, YTDL_DEFAULT_FORMAT } = require("./settings");
29 | const { version } = require("./package.json");
30 | // Returning cached properties if the CPU usage high.
31 | let cachedProps = {};
32 |
33 | const argv = yargs
34 | .option("address", {
35 | description: "Server address to listen on",
36 | type: "string",
37 | })
38 | .option("webport", {
39 | description: "First available server port",
40 | alias: "p",
41 | type: "number",
42 | default: 8000,
43 | })
44 | .option("webportrangeend", {
45 | description: "Last available server port",
46 | alias: "e",
47 | type: "number",
48 | default: 8005,
49 | })
50 | .option("uselocaldb", {
51 | description: "Use database for storing collection & mediastatus",
52 | type: "boolean",
53 | default: false,
54 | })
55 | .option("filebrowserpaths", {
56 | description: "File browser paths, which can be accessed by the server",
57 | type: "array",
58 | })
59 | .option("unsafefilebrowsing", {
60 | description: "Allows to browse your filesystem",
61 | type: "boolean",
62 | default: false,
63 | })
64 | .option("verbose", {
65 | description: "Activates MPV node verbose log",
66 | type: "boolean",
67 | default: false,
68 | })
69 | .option("osd-messages", {
70 | description: "Enables OSD messages",
71 | type: "boolean",
72 | default: false,
73 | })
74 | .help()
75 | .alias("help", "h").argv;
76 | if (argv._.length == 0) {
77 | console.log("No socket provided");
78 | process.exit();
79 | }
80 |
81 | loadSettings(argv);
82 |
83 | const app = express();
84 | app.use(cors(CORSOPTIONS));
85 | app.use(express.json());
86 |
87 | app.use("/", filebrowser);
88 | app.use("/api/v1/collections", collections);
89 |
90 | const mpv = new mpvAPI({
91 | socket: settings.socketName,
92 | verbose: settings.verbose,
93 | });
94 |
95 | function stringIsAValidUrl(s) {
96 | try {
97 | new URL(s);
98 | return true;
99 | } catch (err) {
100 | return false;
101 | }
102 | }
103 |
104 | // Thanks: https://javascript.plainenglish.io/how-to-add-a-timeout-limit-to-asynchronous-javascript-functions-3676d89c186d
105 | const asyncCallWithTimeout = async (asyncPromise, timeLimit) => {
106 | let timeoutHandle;
107 |
108 | const timeoutPromise = new Promise((_resolve, reject) => {
109 | timeoutHandle = setTimeout(
110 | () => reject(new Error("Async call timeout limit reached")),
111 | timeLimit
112 | );
113 | });
114 |
115 | return Promise.race([asyncPromise, timeoutPromise]).then((result) => {
116 | clearTimeout(timeoutHandle);
117 | return result;
118 | });
119 | };
120 |
121 | app.get("/api/v1/status", async (req, res) => {
122 | try {
123 | const result = await asyncCallWithTimeout(
124 | getMPVProps(req.query.exclude),
125 | 500
126 | );
127 | // Returning cached properties if the CPU usage high.
128 | cachedProps = Object.assign(cachedProps, result);
129 | return res.json(result);
130 | } catch (exc) {
131 | if (exc.message == "Async call timeout limit reached")
132 | return res.json(cachedProps);
133 | else return res.status(500).json({ message: exc });
134 | }
135 | });
136 |
137 | /*
138 | MEDIA CONTROLS
139 | */
140 |
141 | app.post("/api/v1/controls/play-pause", async (req, res) => {
142 | try {
143 | await mpv.togglePause();
144 | return res.json({ message: "success" });
145 | } catch (exc) {
146 | console.log(exc);
147 | return res.status(500).json({ message: exc });
148 | }
149 | });
150 |
151 | app.post("/api/v1/controls/play", async (req, res) => {
152 | try {
153 | await mpv.play();
154 | return res.json({ messsage: "success" });
155 | } catch (exc) {
156 | return res.status(500).json({ message: exc });
157 | }
158 | });
159 |
160 | app.post("/api/v1/controls/pause", async (req, res) => {
161 | try {
162 | await mpv.pause();
163 | return res.json({ messsage: "success" });
164 | } catch (exc) {
165 | return res.status(500).json({ message: exc });
166 | }
167 | });
168 |
169 | app.post("/api/v1/controls/stop", async (req, res) => {
170 | try {
171 | await mpv.stop();
172 | return res.json({ message: "success" });
173 | } catch (exc) {
174 | console.log(exc);
175 | return res.status(500).json({ message: exc });
176 | }
177 | });
178 |
179 | async function playlistPrev(req, res) {
180 | try {
181 | await mpv.prev();
182 | return res.json({ message: "success" });
183 | } catch (exc) {
184 | console.log(exc);
185 | return res.status(500).json({ message: exc });
186 | }
187 | }
188 |
189 | async function playlistNext(req, res) {
190 | try {
191 | await mpv.next();
192 | return res.json({ message: "success" });
193 | } catch (exc) {
194 | console.log(exc);
195 | return res.status(500).json({ message: exc });
196 | }
197 | }
198 |
199 | app.post("/api/v1/controls/prev", playlistPrev);
200 | app.post("/api/v1/playlist/prev", playlistPrev);
201 |
202 | app.post("/api/v1/controls/next", playlistNext);
203 | app.post("/api/v1/playlist/next", playlistNext);
204 |
205 | app.post("/api/v1/controls/fullscreen", async (req, res) => {
206 | try {
207 | await mpv.toggleFullscreen();
208 | return res.json({ message: "success" });
209 | } catch (exc) {
210 | console.log(exc);
211 | return res.status(500).json({ message: exc });
212 | }
213 | });
214 |
215 | app.post("/api/v1/controls/volume/:value", async (req, res) => {
216 | try {
217 | await mpv.volume(req.params.value);
218 | return res.json({ message: "success" });
219 | } catch (exc) {
220 | console.log(exc);
221 | return res.status(500).json({ message: exc });
222 | }
223 | });
224 |
225 | app.post("/api/v1/controls/mute", async (req, res) => {
226 | try {
227 | await mpv.mute();
228 | return res.json({ message: "success" });
229 | } catch (exc) {
230 | console.log(exc);
231 | return res.status(500).json({ message: exc });
232 | }
233 | });
234 |
235 | app.post("/api/v1/controls/seek", async (req, res) => {
236 | try {
237 | if (!req.body.flag) req.body.flag = "relative";
238 | await mpv.seek(req.body.target, req.body.flag);
239 | return res.json({ message: "success" });
240 | } catch (exc) {
241 | console.log(exc);
242 | return res.status(500).json({ message: exc });
243 | }
244 | });
245 |
246 | /*
247 | TRACKS
248 | */
249 | app.get("/api/v1/tracks", async (req, res) => {
250 | try {
251 | return res.json(await getTracks());
252 | } catch (exc) {
253 | console.log(exc);
254 | return res.status(500).json({ message: exc });
255 | }
256 | });
257 |
258 | /*
259 | AUDIO TRACKS
260 | */
261 | app.post("/api/v1/tracks/audio/reload/:id", async (req, res) => {
262 | try {
263 | await mpv.selectAudioTrack(req.params.id);
264 | return res.json({ message: "success" });
265 | } catch (exc) {
266 | console.log(exc);
267 | return res.status(500).json({ message: exc });
268 | }
269 | });
270 |
271 | app.post("/api/v1/tracks/audio/cycle", async (req, res) => {
272 | try {
273 | await mpv.cycleAudioTracks();
274 | return res.json({ message: "success" });
275 | } catch (exc) {
276 | console.log(exc);
277 | return res.status(500).json({ message: exc });
278 | }
279 | });
280 |
281 | app.post("/api/v1/tracks/audio/add", async (req, res) => {
282 | try {
283 | if (!req.body.flag) req.body.flag = "select";
284 | await mpv.addAudioTrack(req.body.filename, req.body.flag);
285 | return res.json({ message: "success" });
286 | } catch (exc) {
287 | console.log(exc);
288 | return res.status(500).json({ message: exc });
289 | }
290 | });
291 |
292 | app.post("/api/v1/tracks/audio/timing/:seconds", async (req, res) => {
293 | try {
294 | await mpv.adjustAudioTiming(req.params.seconds);
295 | return res.json({ message: "success" });
296 | } catch (exc) {
297 | console.log(exc);
298 | return res.status(500).json({ message: exc });
299 | }
300 | });
301 |
302 | /*
303 | SUB TRACKS
304 | */
305 | app.post("/api/v1/tracks/sub/timing/:seconds", async (req, res) => {
306 | try {
307 | await mpv.adjustSubtitleTiming(req.params.seconds);
308 | return res.json({ message: "success" });
309 | } catch (exc) {
310 | console.log(exc);
311 | return res.status(500).json({ message: exc });
312 | }
313 | });
314 |
315 | app.post("/api/v1/tracks/sub/ass-override/:value", async (req, res) => {
316 | try {
317 | await mpv.setProperty("sub-ass-override", req.params.value);
318 | return res.json({ message: "success" });
319 | } catch (exc) {
320 | console.log(exc);
321 | return res.status(500).json({ message: exc });
322 | }
323 | });
324 |
325 | app.post("/api/v1/tracks/sub/font-size/:size", async (req, res) => {
326 | try {
327 | await mpv.setProperty("sub-font-size", req.params.size);
328 | return res.json({ message: "success" });
329 | } catch (exc) {
330 | console.log(exc);
331 | return res.status(500).json({ message: exc });
332 | }
333 | });
334 |
335 | app.post("/api/v1/tracks/sub/toggle-visibility", async (req, res) => {
336 | try {
337 | await mpv.toggleSubtitleVisibility();
338 | return res.json({ message: "success" });
339 | } catch (exc) {
340 | console.log(exc);
341 | return res.status(500).json({ message: exc });
342 | }
343 | });
344 |
345 | app.post("/api/v1/tracks/sub/visibility/:value", async (req, res) => {
346 | try {
347 | let val = req.params.value.toLowerCase() == "true" ? true : false;
348 | await mpv.setProperty("sub-visibility", val);
349 | return res.json({ message: "success" });
350 | } catch (exc) {
351 | console.log(exc);
352 | return res.status(500).json({ message: exc });
353 | }
354 | });
355 |
356 | app.post("/api/v1/tracks/sub/add", async (req, res) => {
357 | try {
358 | // TODO: title, lang
359 | if (!req.body.flag) req.body.flag = "select";
360 | await mpv.addSubtitles(req.body.filename, req.body.flag);
361 | return res.json({ message: "success" });
362 | } catch (exc) {
363 | console.log(exc);
364 | return res.status(500).json({ message: exc });
365 | }
366 | });
367 |
368 | app.post("/api/v1/tracks/sub/reload/:id", async (req, res) => {
369 | try {
370 | await mpv.selectSubtitles(req.params.id);
371 | return res.json({ message: "success" });
372 | } catch (exc) {
373 | console.log(exc);
374 | return res.status(500).json({ message: exc });
375 | }
376 | });
377 |
378 | /*
379 | PLAYLIST
380 | */
381 | app.get("/api/v1/playlist", async (req, res) => {
382 | try {
383 | return res.json(await getPlaylist());
384 | } catch (exc) {
385 | console.log(exc);
386 | return res.status(500).json({ message: exc });
387 | }
388 | });
389 |
390 | async function writeFileLocalOptions(options) {
391 | await fs_async.writeFile(
392 | FILE_LOCAL_OPTIONS_PATH,
393 | JSON.stringify(options),
394 | "utf-8"
395 | );
396 | }
397 |
398 | async function readFileLocalOptions() {
399 | const content = await fs_async.readFile(FILE_LOCAL_OPTIONS_PATH, "utf-8");
400 | return JSON.parse(content);
401 | }
402 |
403 | app.post("/api/v1/playlist", async (req, res) => {
404 | try {
405 | if (!req.body.flag) req.body.flag = "append-play";
406 | if (
407 | !stringIsAValidUrl(req.body.filename) &&
408 | fs.lstatSync(req.body.filename).isDirectory()
409 | ) {
410 | for (const item of await fs_async.readdir(req.body.filename)) {
411 | let type = detectFileType(path.extname(item));
412 | if (type === "video" || type == "audio") {
413 | await mpv.load(item, "append-play");
414 | }
415 | }
416 | } else {
417 | if (req.body["file-local-options"]) {
418 | let fileLocalOptions = await readFileLocalOptions();
419 | fileLocalOptions[req.body.filename] = req.body["file-local-options"];
420 |
421 | if (!req.body["file-local-options"]["ytdl-format"])
422 | req.body["file-local-options"]["ytdl-format"] = YTDL_DEFAULT_FORMAT;
423 |
424 | // Have to write cach file here
425 | await writeFileLocalOptions(fileLocalOptions);
426 | }
427 | await mpv.load(req.body.filename, req.body.flag);
428 | if (req.body.seekTo) {
429 | await mpv.seek(req.body.seekTo, "absolute");
430 | }
431 | }
432 | return res.json({ message: "success" });
433 | } catch (exc) {
434 | console.log(exc);
435 | return res.status(500).json({ message: exc });
436 | }
437 | });
438 |
439 | app.delete("/api/v1/playlist/remove/:index", async (req, res) => {
440 | try {
441 | await mpv.playlistRemove(req.params.index);
442 | return res.json({ message: "success" });
443 | } catch (exc) {
444 | console.log(exc);
445 | return res.status(500).json({ message: exc });
446 | }
447 | });
448 |
449 | app.post("/api/v1/playlist/move", async (req, res) => {
450 | try {
451 | if (!req.query.fromIndex)
452 | return res
453 | .status(400)
454 | .json({ message: "fromIndex query param required!" });
455 | if (!req.query.toIndex)
456 | return res.status(400).json({ message: "toIndex query param required!" });
457 |
458 | await mpv.playlistMove(req.query.fromIndex, req.query.toIndex);
459 | return res.json({ message: "success" });
460 | } catch (exc) {
461 | console.log(exc);
462 | return res.status(500).json({ message: exc });
463 | }
464 | });
465 |
466 | app.post("/api/v1/playlist/play/:index", async (req, res) => {
467 | try {
468 | await mpv.command("playlist-play-index", [req.params.index]);
469 | await mpv.play();
470 | // return res.json({ message: "success" });
471 | return res.json(await getPlaylist());
472 | } catch (exc) {
473 | console.log(exc);
474 | return res.status(500).json({ message: exc });
475 | }
476 | });
477 |
478 | app.post("/api/v1/playlist/clear", async (req, res) => {
479 | try {
480 | await mpv.clearPlaylist();
481 | return res.json({ message: "success" });
482 | } catch (exc) {
483 | console.log(exc);
484 | return res.status(500).json({ message: exc });
485 | }
486 | });
487 |
488 | app.post("/api/v1/playlist/shuffle", async (req, res) => {
489 | try {
490 | await mpv.shuffle();
491 | return res.json({ message: "success" });
492 | } catch (exc) {
493 | console.log(exc);
494 | return res.status(500).json({ message: exc });
495 | }
496 | });
497 |
498 | app.get("/api/v1/mpvinfo", async (req, res) => {
499 | try {
500 | res.json(await getMPVInfo());
501 | } catch (exc) {
502 | return res.status(500).json({ message: exc });
503 | }
504 | });
505 |
506 | async function shutdownAction(action) {
507 | switch (action) {
508 | case "shutdown":
509 | // First stop MPV playback to save playback data
510 | await mpv.stop();
511 | exec(
512 | os.platform == "win32" ? WIN_SHUTDOWN_COMMAND : UNIX_SHUTDOWN_COMMAND
513 | );
514 | break;
515 | case "reboot":
516 | await mpv.stop();
517 | exec(os.platform == "win32" ? WIN_REBOOT_COMMAND : UNIX_REBOOT_COMMAND);
518 | break;
519 | case "quit":
520 | await mpv.stop();
521 |
522 | break;
523 | }
524 | }
525 |
526 | app.post("/api/v1/computer/:action", async (req, res) => {
527 | try {
528 | switch (req.params.action) {
529 | case "shutdown":
530 | case "reboot":
531 | case "quit":
532 | shutdownAction(req.params.action);
533 | break;
534 | default:
535 | return res.status(400).json({ message: "Invalid action" });
536 | }
537 | } catch (exc) {
538 | console.log(exc);
539 | }
540 | });
541 |
542 | mpv.on("status", async (status) => {
543 | try {
544 | switch (status.property) {
545 | case "pause":
546 | await showOSDMessage(status.value ? "Pause" : "Play");
547 | break;
548 | case "volume":
549 | await showOSDMessage(`Volume: ${status.value}%`);
550 | break;
551 | case "mute":
552 | let volume = await mpv.getProperty("volume");
553 | await showOSDMessage(status.value ? "Mute" : `Volume ${volume}`);
554 | break;
555 | case "playlist-count":
556 | case "playlist-pos":
557 | break;
558 | case "path":
559 | playerData = await getMPVProps();
560 | if (status.value) {
561 | // Reset subdelay to 0
562 | await mpv.setProperty("sub-delay", 0);
563 | // Also reset audio delay to 0
564 | await mpv.adjustAudioTiming(0);
565 | await showOSDMessage(
566 | `Playing: ${playerData["media-title"] || playerData.filename}`
567 | );
568 | }
569 | break;
570 | }
571 | } catch (exc) {
572 | console.log(exc);
573 | }
574 | });
575 |
576 | mpv.on("seek", async (data) => {
577 | await showOSDMessage(`Seek: ${formatTime(data.end)}`);
578 | });
579 |
580 | function formatTime(param) {
581 | var sec_num = parseInt(param);
582 | var hours = Math.floor(sec_num / 3600);
583 | var minutes = Math.floor((sec_num - hours * 3600) / 60);
584 | var seconds = sec_num - hours * 3600 - minutes * 60;
585 |
586 | if (hours < 10) {
587 | hours = "0" + hours;
588 | }
589 | if (minutes < 10) {
590 | minutes = "0" + minutes;
591 | }
592 | if (seconds < 10) {
593 | seconds = "0" + seconds;
594 | }
595 | return hours + ":" + minutes + ":" + seconds;
596 | }
597 |
598 | function handle(promise) {
599 | return promise
600 | .then((data) => [data, undefined])
601 | .catch((error) => Promise.resolve([undefined, error]));
602 | }
603 |
604 | async function getMPVInfo() {
605 | return {
606 | "ffmpeg-version": await handle(mpv.getProperty("ffmpeg-version"))
607 | .then((resp) => resp[0])
608 | .catch(() => null),
609 | "mpv-version": await handle(mpv.getProperty("mpv-version"))
610 | .then((resp) => resp[0])
611 | .catch(() => null),
612 | "libass-version": await handle(mpv.getProperty("libass-version"))
613 | .then((resp) => resp[0])
614 | .catch(() => null),
615 | mpvremoteConfig: settings,
616 | mpvremoteVersion: version,
617 | };
618 | }
619 |
620 | async function getTracks() {
621 | const count = await mpv.getProperty("track-list/count");
622 | let tracks = [];
623 | for (let i = 0; i < count; i++) {
624 | try {
625 | let track = {
626 | index: i,
627 | id: await handle(mpv.getProperty(`track-list/${i}/id`)).then(
628 | (resp) => resp[0]
629 | ),
630 | type: await handle(mpv.getProperty(`track-list/${i}/type`)).then(
631 | (resp) => resp[0]
632 | ),
633 | selected: await handle(
634 | mpv.getProperty(`track-list/${i}/selected`)
635 | ).then((resp) => resp[0]),
636 | codec: await handle(mpv.getProperty(`track-list/${i}/codec`)).then(
637 | (resp) => resp[0]
638 | ),
639 | };
640 | // Get specific stuff
641 | if (track.type === "video") {
642 | track["demux-w"] = await handle(
643 | mpv.getProperty(`track-list/${i}/demux-w`)
644 | ).then((resp) => resp[0]);
645 | track["demux-h"] = await handle(
646 | mpv.getProperty(`track-list/${i}/demux-h`)
647 | ).then((resp) => resp[0]);
648 | } else if (track.type === "audio") {
649 | track["demux-channel-count"] = await handle(
650 | mpv.getProperty(`track-list/${i}/demux-channel-count`)
651 | ).then((resp) => resp[0]);
652 | track["demux-channels"] = await handle(
653 | mpv.getProperty(`track-list/${i}/demux-channels`)
654 | ).then((resp) => resp[0]);
655 | track["demux-samplerate"] = await handle(
656 | mpv.getProperty(`track-list/${i}/demux-samplerate`)
657 | ).then((resp) => resp[0]);
658 | track["demux-bitrate"] = await handle(
659 | mpv.getProperty(`track-list/${i}/demux-bitrate`)
660 | ).then((resp) => resp[0]);
661 | track.lang = await handle(mpv.getProperty(`track-list/${i}/lang`)).then(
662 | (resp) => resp[0]
663 | );
664 | track["external-filename"] = await handle(
665 | mpv.getProperty(`track-list/${i}/external-filename`)
666 | ).then((resp) => resp[0]);
667 | } else if (track.type === "sub") {
668 | track.lang = await handle(mpv.getProperty(`track-list/${i}/lang`)).then(
669 | (resp) => resp[0]
670 | );
671 | track["external-filename"] = await handle(
672 | mpv.getProperty(`track-list/${i}/external-filename`)
673 | ).then((resp) => resp[0]);
674 | }
675 |
676 | tracks.push(track);
677 | } catch (exc) {
678 | console.log(exc);
679 | }
680 | }
681 | return tracks;
682 | }
683 |
684 | async function getPlaylist() {
685 | const count = await mpv.getProperty("playlist-count");
686 | let playlist = [];
687 | for (let i = 0; i < count; i++) {
688 | try {
689 | let item = {
690 | index: i,
691 | id: await handle(mpv.getProperty(`playlist/${i}/id`)).then(
692 | (resp) => resp[0]
693 | ),
694 | filePath: await handle(mpv.getProperty(`playlist/${i}/filename`)).then(
695 | (resp) => resp[0]
696 | ),
697 | current: await handle(mpv.getProperty(`playlist/${i}/current`)).then(
698 | (resp) => resp[0]
699 | ),
700 | title: await handle(mpv.getProperty(`playlist/${i}/title`)).then(
701 | (resp) => resp[0]
702 | ),
703 | };
704 |
705 | if (item.filePath) item.filename = path.basename(item.filePath);
706 | playlist.push(item);
707 | } catch (exc) {
708 | console.log(exc);
709 | }
710 | }
711 | return playlist;
712 | }
713 |
714 | async function getChapters() {
715 | const count = await mpv.getProperty("chapter-list/count");
716 | let chapters = [];
717 | for (let i = 0; i < count; i++) {
718 | chapters.push({
719 | title: await handle(mpv.getProperty(`chapter-list/${i}/title`)).then(
720 | (resp) => resp[0]
721 | ),
722 | time: await handle(mpv.getProperty(`chapter-list/${i}/time`)).then(
723 | (resp) => resp[0]
724 | ),
725 | });
726 | }
727 | return chapters;
728 | }
729 |
730 | async function getMetaData() {
731 | const count = await mpv.getProperty("metadata/list/count");
732 | let metadata = {};
733 | for (let i = 0; i < count; i++) {
734 | const key = await handle(mpv.getProperty(`metadata/list/${i}/key`)).then(
735 | (resp) => resp[0]
736 | );
737 | if (key) {
738 | const value = await handle(
739 | mpv.getProperty(`metadata/list/${i}/value`)
740 | ).then((resp) => resp[0]);
741 |
742 | if (value) {
743 | metadata[key] = value;
744 | }
745 | }
746 | }
747 | return metadata;
748 | }
749 |
750 | async function getMPVProp(key) {
751 | try {
752 | switch (key) {
753 | case "playlist":
754 | return await getPlaylist();
755 | case "chapter-list":
756 | return await getChapters();
757 | case "track-list":
758 | return await getTracks();
759 | case "metadata":
760 | return await getMetaData();
761 | case "position":
762 | return await mpv.getProperty("time-pos");
763 | case "remaining":
764 | return await mpv.getProperty("time-remaining");
765 | default:
766 | return await mpv.getProperty(key);
767 | }
768 | } catch (exc) {
769 | if (exc.errmessage != "property unavailable") {
770 | console.log(exc);
771 | }
772 | return null;
773 | }
774 | }
775 |
776 | async function getMPVProps(exclude = []) {
777 | let props = {
778 | pause: false,
779 | mute: false,
780 | filename: null,
781 | duration: 0,
782 | position: 0,
783 | remaining: 0,
784 | "media-title": null,
785 | chapter: 0,
786 | volume: 0,
787 | "volume-max": 100,
788 | fullscreen: false,
789 | speed: 1,
790 | "sub-delay": 0,
791 | "sub-visibility": true,
792 | "audio-delay": 0,
793 | "sub-font-size": 55,
794 | "sub-ass-override": "no",
795 | playlist: [],
796 | "chapter-list": [],
797 | "track-list": [],
798 | metadata: {},
799 | };
800 |
801 | retval = {};
802 | for (key of Object.keys(props)) {
803 | if (!exclude.includes(key)) {
804 | const val = (await getMPVProp(key)) || props[key];
805 | retval[key] = val;
806 | }
807 | }
808 | return retval;
809 | }
810 |
811 | portfinder
812 | .getPortPromise({
813 | port: settings.serverPort,
814 | stopPort: settings.serverPortRangeEnd,
815 | })
816 | .then((port) => {
817 | app.listen(port, settings.realServerIP, () => {
818 | settings.serverPort = port;
819 | console.log(`listening on ${settings.serverIP}:${port}`);
820 | main();
821 | });
822 | })
823 | .catch(() => {
824 | console.log(
825 | "There is no free port available, mpv-remote not started check your settings."
826 | );
827 | });
828 |
829 | async function showOSDMessage(text, timeout = null) {
830 | /*
831 | Shows OSD message on MPV if it's enabled on settings.
832 | */
833 | if (settings.osdMessages) {
834 | if (timeout) return await mpv.command("show-text", [text, timeout]);
835 | else return await mpv.command("show-text", [text]);
836 | } else {
837 | console.log(`OSD message: ${text}`);
838 | }
839 | }
840 |
841 | async function main() {
842 | try {
843 | // Creates/clears file local options file.
844 | await mpv.start();
845 |
846 | // Create file-local-options if not exists.
847 | if (!fs.existsSync(FILE_LOCAL_OPTIONS_PATH)) writeFileLocalOptions({});
848 | if (settings.uselocaldb) await initDB();
849 |
850 | await showOSDMessage(
851 | `Remote access on: ${settings.serverIP}:${settings.serverPort}`,
852 | 5000
853 | );
854 | } catch (error) {
855 | // handle errors here
856 | console.log(error);
857 | }
858 | }
859 |
860 | process.on("unhandledRejection", (error) => {
861 | // Will print "unhandledRejection err is not defined"
862 | console.log("unhandledRejection", JSON.stringify(error));
863 | });
864 |
--------------------------------------------------------------------------------
/settings.js:
--------------------------------------------------------------------------------
1 | const { networkInterfaces } = require("os");
2 | const { readFileSync, existsSync } = require("fs");
3 | const { getMPVHome } = require("./crud");
4 |
5 | const path = require("path");
6 |
7 | const IP_ADDR = Object.values(networkInterfaces())
8 | .flat()
9 | .find((i) => (i.family == "IPv4" || i.family == 4) && !i.internal);
10 |
11 | const CORSOPTIONS = {
12 | origin: "*",
13 | methods: ["GET", "POST", "DELETE", "UPDATE", "PUT", "PATCH"],
14 | };
15 |
16 | let settings = {
17 | serverIP: IP_ADDR ? IP_ADDR.address : "127.0.0.1", // Used for displaying the remote access URL
18 | realServerIP: undefined, // Used for app.listen(). Default is all interfaces
19 | serverPort: null,
20 | serverPortRangeEnd: null,
21 | filebrowserPaths: [],
22 | socketName: null,
23 | uselocaldb: false,
24 | unsafefilebrowsing: false,
25 | verbose: false,
26 | };
27 |
28 | /*
29 | Loads settings
30 | */
31 | function loadSettings(argv) {
32 | settings.socketName = argv._[0];
33 | settings.realServerIP = argv.address;
34 | // If we have an explicit address, display that instead
35 | if (argv.address) settings.serverIP = argv.address;
36 | settings.serverPort = argv.webport;
37 | settings.serverPortRangeEnd = argv.webportrangeend;
38 | settings.uselocaldb = argv.uselocaldb;
39 | settings.unsafefilebrowsing = argv.unsafefilebrowsing;
40 | settings.verbose = argv.verbose;
41 | settings.osdMessages = argv["osd-messages"];
42 |
43 | if (argv.filebrowserpaths) {
44 | settings.filebrowserPaths = argv.filebrowserpaths.map((el, index) => {
45 | return {
46 | index,
47 | path: el.replace(/^"|'+|"|'+$/g, ""),
48 | };
49 | });
50 | }
51 | }
52 |
53 | /*
54 | Load default ytdl format,
55 | if it don't exist return empty string
56 | */
57 | function readDefaultYtdlFormat() {
58 | const mpvConfigPath = path.join(getMPVHome(), "mpv.conf");
59 | if (!existsSync(mpvConfigPath)) {
60 | console.log("No mpv.conf file found");
61 | return ""
62 | }
63 |
64 | const ytdlFormatLine =
65 | readFileSync(mpvConfigPath, "utf8")
66 | .split("\n")
67 | .find((line) => line.includes("ytdl-format"))
68 |
69 | if (!ytdlFormatLine) {
70 | console.log("No ytdl-format line found in mpv.conf")
71 | return "";
72 | }
73 | const regex = /ytdl-format="(.+?)"/;
74 | const match = ytdlFormatLine.match(regex);
75 |
76 | if (match)
77 | return match[1];
78 | return "";
79 | }
80 |
81 |
82 | exports.loadSettings = loadSettings;
83 | exports.settings = settings;
84 | exports.CORSOPTIONS = CORSOPTIONS;
85 | exports.YTDL_DEFAULT_FORMAT = readDefaultYtdlFormat();
86 |
--------------------------------------------------------------------------------
/watchlisthandler.js:
--------------------------------------------------------------------------------
1 | const process = require("process");
2 | const { initDB, addMediaStatusEntry } = require("./crud");
3 |
4 | console.log("Watchlisthandler called");
5 | async function main() {
6 | await initDB();
7 | /*
8 | Required parameters:
9 | [0]: Filepath (Full path)
10 | [1]: Time
11 | [2]: percent-pos
12 | */
13 | const cliArgs = process.argv.slice(2);
14 | if (cliArgs.length < 3) {
15 | console.log("Not enough parameters");
16 | process.exit();
17 | }
18 | await addMediaStatusEntry(cliArgs[0], cliArgs[1], cliArgs[2]);
19 | console.log("Entry added/updated");
20 | }
21 |
22 | main();
23 |
--------------------------------------------------------------------------------