-
Notifications
You must be signed in to change notification settings - Fork 3
/
PlayOn.py
7146 lines (6815 loc) · 329 KB
/
PlayOn.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# DLNAPlayOn v1.8.4 (https://github.com/PCigales/DLNAPlayOn)
# Copyright © 2022 PCigales
# This program is licensed under the GNU GPLv3 copyleft license (see https://www.gnu.org/licenses)
from functools import partial
import threading
import selectors
import argparse
import os
import msvcrt
import subprocess
import time
from http import server, client, HTTPStatus
import socket
import socketserver
import ssl
import urllib.request, urllib.parse, urllib.error
from io import BytesIO
from xml.dom import minidom
import json
import html
import struct
import array
import hashlib
import base64
import re
import webbrowser
import mimetypes
import random
import locale
import ctypes, ctypes.wintypes
FR_STRINGS = {
'mediaprovider': {
'opening': 'Ouverture de "%s" reconnu comme "%s" en mode "%s" - titre: %s',
'extension': 'Extension de "%s" retenue comme "%s"',
'failure': 'Échec de l\'ouverture de "%s" en tant que "%s"',
'subopening': 'Ouverture des sous-titres "%s" reconnus comme "%s"',
'subextension': 'Extension des sous_titres retenue comme "%s"',
'subfailure': 'Échec de l\'ouverture des sous-titres "%s" en tant que "%s"',
'contentpath': 'chemin d\'accès de contenu',
'contenturl': 'url de contenu',
'webpageurl': 'url de page web',
'loadstart': 'Début du chargement dans le tampon du contenu',
'segmentbuffering': 'Segment %d -> placement dans la zone %d du tampon',
'segmentfailure': 'Segment %d -> échec de lecture du contenu',
'loadstop': 'Fin du chargement dans le tampon du contenu',
'loadinterrupt': 'Interruption du chargement dans le tampon du contenu',
'connection': 'Connexion pour diffusion de "%s": persistente = %s - requêtes partielles = %s',
'yes': 'oui',
'no': 'non',
'indexation': 'Indexation du tampon sur la connexion %d',
'deindexation': 'Désindexation du tampon',
'translation': 'Translation du tampon vers la position %d',
'present': 'Segment %d -> déjà présent dans la zone %d du tampon'
},
'mediaserver': {
'connection': 'Connexion au serveur de diffusion de %s:%s',
'deliverystart': 'Connexion %d -> début de la distribution du contenu à %s:%s',
'delivery1': 'Connexion %d -> segment %d -> distribution à partir de la zone %d du tampon',
'delivery2': 'Connexion %d -> segment %d -> distribution',
'delivery3': 'Connexion %d -> segment %d -> distribution à partir du tampon',
'exceeded': 'Connexion %d -> segment %d -> la zone %d a été dépassée par la queue du tampon',
'expulsion': 'Connexion %d -> segment %d -> expulsion du tampon',
'failure': 'Connexion %d -> segment %d -> échec de distribution du contenu',
'deliveryfailure': 'Connexion %d -> échec de distribution du contenu',
'deliverystop': 'Connexion %d -> fin de la distribution du contenu',
'subdelivery': 'Distribution des sous-titres à %s:%s',
'subfailure': 'Échec de distribution des sous-titres à %s:%s',
'start': 'Démarrage, sur l\'interface %s, du serveur de diffusion en mode %s%s',
'sequential': 'séquentiel',
'random': 'aléatoire',
'unsupported': ' non supporté par la source',
'shutdown': 'Fermeture du serveur de diffusion'
},
'dlnanotification': {
'start': 'Démarrage du serveur d\'écoute des notifications d\'événement de %s DLNA à l\'adresse %s:%s',
'stop': 'Arrêt du serveur d\'écoute des notifications d\'événement de %s DLNA à l\'adresse %s:%s',
'alreadyactivated': 'Serveur d\'écoute des notifications d\'événement de %s DLNA à l\'adresse %s:%s déjà activée',
'receipt': 'DLNA Renderer %s -> service %s -> réception de la notification d\'événement %s',
'notification': 'DLNA Renderer %s -> Service %s -> notification d\'événement %s -> %s est passé à %s',
'alert': 'DLNA Renderer %s -> Service %s -> notification d\'événement %s -> alerte: %s est passé à %s'
},
'dlnaadvertisement': {
'receipt': 'Réception, sur l\'interface %s, d\'une publicité du périphérique %s (%s:%s): %s',
'ignored': 'Publicité du périphérique %s (%s:%s) ignorée en raison de la discordance d\'adresse de l\'URL de description',
'set': 'Mise en place de l\'écoute des publicités de périphérique DLNA sur l\'interface %s',
'fail': 'Échec de la mise en place de l\'écoute des publicités de périphérique DLNA sur l\'interface %s',
'alreadyactivated': 'Écoute des publicités de périphérique DLNA déjà activée',
'start': 'Démarrage du serveur d\'écoute des publicités de périphérique DLNA',
'stop': 'Arrêt du serveur d\'écoute des publicités de périphérique DLNA'
},
'dlnahandler': {
'ip_failure': 'Échec de la récupération de l\'adresse ip de l\'hôte',
'registering': 'Enregistrement du %s %s sur l\'interface %s',
'msearch1': 'Envoi d\'un message de recherche de uuid:%s',
'msearch2': 'Envoi d\'un message de recherche de périphérique DLNA',
'msearch3': 'Envoi d\'un message de recherche de %s DLNA',
'sent': 'Envoi du message de recherche sur l\'interface %s',
'fail': 'Échec de l\'envoi du message de recherche sur l\'interface %s',
'receipt': 'Réception, sur l\'interface %s, d\'une réponse au message de recherche de %s:%s',
'ignored': 'Réponse de %s:%s ignorée en raison de la discordance d\'adresse de l\'URL de description',
'alreadyactivated': 'Recherche de %s DLNA déjà activée',
'start': 'Démarrage de la recherche de %s DLNA',
'stop': 'Fin de la recherche de %s DLNA',
'commandabandonment': '%s %s -> service %s -> abandon de l\'envoi de la commande %s',
'commandsending': '%s %s -> service %s -> envoi de la commande %s',
'commandfailure': '%s %s -> service %s -> échec de l\'envoi de la commande %s',
'commandsuccess': '%s %s -> service %s -> succès de l\'envoi de la commande %s',
'responsefailure': '%s %s -> service %s -> échec du traitement de la réponse à la commande %s',
'responsesuccess': '%s %s -> service %s -> succès de la réception de la réponse à la commande %s',
'advertalreadyactivated': 'Écoute des publicités de %s déjà activée',
'advertstart': 'Démarrage de l\'écoute des publicités de %s',
'advertstop': 'Fin de l\'écoute des publicités de %s',
'subscralreadyactivated': 'Renderer %s -> service %s -> souscription au serveur d\'événements déjà en place',
'subscrfailure': 'Renderer %s -> service %s -> échec de la demande de souscription au serveur d\'événements',
'subscrsuccess': 'Renderer %s -> service %s -> souscription au serveur d\'événements sous le SID %s pour une durée de %s',
'subscrrenewfailure': 'Renderer %s -> service %s -> échec de la demande de renouvellement de souscription de SID %s au serveur d\'événements',
'subscrrenewsuccess': 'Renderer %s -> service %s -> renouvellement de la souscription de SID %s au serveur d\'événements pour une durée de %s',
'subscrunsubscrfailure': 'Renderer %s -> service %s -> échec de la demande de fin de souscription de SID %s au serveur d\'événements',
'subscrunsubscrsuccess': 'Renderer %s -> service %s -> fin de la souscription de SID %s au serveur d\'événements'
},
'websocket': {
'endacksuccess': 'WebSocket serveur %s:%s/%s -> WebSocket %s:%s -> succès de l\'envoi de l\'accusé de réception de l\'avis de fin de connexion',
'endackfailure': 'WebSocket serveur %s:%s/%s -> WebSocket %s:%s -> échec de l\'envoi de l\'accusé de réception de l\'avis de fin de connexion',
'errorendnotification': 'WebSocket serveur %s:%s/%s -> WebSocket %s:%s -> envoi d\'avis de fin de connexion pour cause d\'erreur %s',
'errorendnotificationsuccess': 'WebSocket serveur %s:%s/%s -> WebSocket %s:%s -> succès de l\'envoi de l\'avis de fin de connexion',
'errorendnotificationfailure': 'WebSocket serveur %s:%s/%s -> WebSocket %s:%s -> échec de l\'envoi de l\'avis de fin de connexion',
'terminationdatasuccess': 'WebSocket serveur %s:%s/%s -> WebSocket %s:%s -> succès de l\'envoi de la donnée de terminaison %s',
'terminationdatafailure': 'WebSocket serveur %s:%s/%s -> WebSocket %s:%s -> échec de l\'envoi de la donnée de terminaison %s',
'endnotificationsuccess': 'WebSocket serveur %s:%s/%s -> WebSocket %s:%s -> succès de l\'envoi de l\'avis de fin de connexion',
'endnotificationfailure': 'WebSocket serveur %s:%s/%s -> WebSocket %s:%s -> échec de l\'envoi de l\'avis de fin de connexion',
'datasuccess': 'WebSocket serveur %s:%s/%s -> WebSocket %s:%s -> envoi de la donnée %s',
'datafailure': 'WebSocket serveur %s:%s/%s -> WebSocket %s:%s -> échec de l\'envoi de la donnée %s',
'datareceipt': 'WebSocket serveur %s:%s/%s -> WebSocket %s:%s -> réception de la donnée %s',
'connectionrequest': 'WebSocket serveur %s:%s -> demande de connexion du WebSocket %s:%s',
'connectionrequestinvalid': 'WebSocket serveur %s:%s -> demande de connexion du WebSocket %s:%s invalide',
'connectionrequestnotfound': 'WebSocket serveur %s:%s -> chemin de demande de connexion du WebSocket %s:%s introuvable: /%s',
'connectionresponsefailure': 'WebSocket serveur %s:%s/%s -> échec de l\'envoi de la réponse à la demande de connexion du WebSocket %s:%s',
'connection': 'WebSocket serveur %s:%s/%s -> connexion au WebSocket %s:%s',
'endack': 'WebSocket serveur %s:%s/%s -> accusé de réception de fin de connexion du WebSocket %s:%s',
'endnotification': 'WebSocket serveur %s:%s/%s -> avis de fin de connexion du WebSocket %s:%s',
'connectionend': 'WebSocket serveur %s:%s/%s -> fin de connexion au WebSocket %s:%s',
'start': 'Démarrage du serveur pour Websocket à l\'adresse %s:%s',
'fail': 'Échec du démarrage du serveur pour Websocket à l\'adresse %s:%s',
'open': 'Websocket serveur %s:%s: ouverture du canal /%s',
'close': 'Websocket serveur %s:%s: fermeture du canal /%s',
'shutdown': 'Fermeture du serveur pour Websocket à l\'adresse %s:%s'
},
'webinterface': {
'ip_failure': 'Échec de la récupération de l\'adresse ip de l\'hôte',
'connection': 'Connexion de l\'interface web %s:%s',
'response': 'Réponse à l\'interface Web %s:%s - requête: %s',
'formdatareceipt': 'Réception de la donnée de formulaire %s de %s:%s',
'formdatareject': 'Rejet de la donnée de formulaire %s de %s:%s',
'playbackaccept': 'Prise en compte de la demande de lecture de %s%s à partir de %s sur %s de %s:%s',
'playbacksub': ' et %s',
'playbackreject': 'Rejet de la demande de lecture de %s%s à partir de %s sur %s de %s:%s',
'rendererstart': 'Démarrage du gestionnaire d\'affichage de renderer pour serveur d\'interface Web',
'launchrendererstart': 'Démarrage du gestionnaire d\'affichage de renderer dans le formulaire de lancement pour serveur d\'interface Web',
'rendererstop': 'Arrêt du gestionnaire d\'affichage de renderer pour serveur d\'interface Web',
'controlstart': 'Démarrage du gestionnaire de contrôleur de lecture pour serveur d\'interface Web',
'controlinterrupt': 'Interruption du gestionnaire de contrôleur de lecture pour serveur d\'interface Web',
'controlrenderer': 'Sélection du renderer %s sur l\'interface %s',
'playlist': 'Liste de lecture générée depuis l\'adresse %s: %s contenus média',
'nocontent': 'Absence de contenu média sous l\'adresse %s',
'nonegapless': 'Absence de support de la lecture sans blanc par le renderer %s',
'gapless': 'Lecture sans blanc des contenus média depuis l\'adresse %s activée',
'nogapless': 'Adresse %s incompatible avec la lecture sans blanc',
'norendereranswer': 'Absence de réponse du renderer %s',
'ready': 'Prêt pour lecture de "%s"%s, par %s, sur le renderer "%s"',
'subtitled': ', sous-titrée',
'direct': 'transmission directe de l\'adresse',
'random': 'diffusion via serveur en mode accès aléatoire',
'sequential': 'diffusion via serveur en mode accès séquentiel%s',
'remuxed': ', remuxé en %s',
'controlstop': 'Arrêt du gestionnaire de contrôleur de lecture pour serveur d\'interface Web%s%s%s%s',
'status': ' - statut ',
'start': 'Démarrage du serveur d\'interface Web sur l\'interface %s',
'alreadyrunning': 'Serveur d\'interface Web déjà en place',
'fail': 'Échec du démarrage du serveur d\'interface Web sur l\'interface %s',
'shutdown': 'Fermeture du serveur d\'interface Web',
'jstart': 'Interface de démarrage',
'jcontrol': 'Interface de contrôle',
'jrenderers': 'Renderers',
'jmwebsocketfailure': 'Échec de l\'établissement de la connexion WebSocket',
'jmrenderersclosed': 'Renderers - interface close',
'jmentervalidurl': 'Saisissez une URL de contenu média valide',
'jmentervalidsuburl': 'Saisissez une URL de sous-titres valide',
'jmselectrenderer': 'Sélectionnez d\'abord un renderer',
'jplaybackposition': 'Position de lecture',
'jgoplay': 'Lire',
'jreset': 'Réinitialiser',
'jplay': 'Lecture',
'jpause': 'Pause',
'jstop': 'Arrêt',
'jinterfaceclosed': 'interface close',
'jback': 'retour',
'jinitialization': 'initialisation',
'jready': 'prêt',
'jreadyfromstart': 'prêt (lecture à partir du début)',
'jinprogress': 'en cours',
'jinplayback': 'lecture',
'jinpause': 'pause',
'jinstop': 'arrêt',
'jplaylist': 'Liste de lecture',
'jurl': 'URL du média',
'jstatus': 'Statut',
'jtargetposition': 'Position cible',
'jmstop': 'Arrêter la lecture ?'
},
'parser': {
'license': 'Ce programme est sous licence copyleft GNU GPLv3 (voir https://www.gnu.org/licenses)',
'help': 'affichage du message d\'aide et interruption du script',
'ip': 'adresse IP de l\'interface web [auto-sélectionnée par défaut - "0.0.0.0", soit toutes les interfaces, si option présente sans mention d\'adresse]',
'port': 'port TCP de l\'interface web [8000 par défaut]',
'hip': 'adresse IP du contrôleur/client DLNA [auto-sélectionnée par défaut - "0.0.0.0", soit toutes les interfaces, si option présente sans mention d\'adresse]',
'rendereruuid': 'uuid du renderer [premier renderer sans sélection sur l\'uuid par défaut]',
'renderername': 'nom du renderer [premier renderer sans sélection sur le nom par défaut]',
'servertype': 'type de serveur (a:auto, s:séquentiel, r:aléatoire, g:sans-blanc/aléatoire, n:aucun) [a par défaut]',
'bufferquick': 'taille de bloc de tampon réduite pour un démarrage plus rapide [désactivé par défaut]',
'buffersize': 'taille du tampon en blocs de 1 Mo [75 par défaut]',
'bufferahead': 'taille du sous-tampon de chargement par anticipation en blocs de 1 Mo [25 par défaut]',
'muxcontainer': 'type de conteneur de remuxage précédé de ! pour qu\'il soit systématique [MP4 par défaut]',
'onreadyplay': 'lecture directe dès que le contenu média et le renderer sont prêts [désactivé par défaut]',
'displayrenderers': 'Affiche les renderers présents sur le réseau',
'start': 'Démarre l\'interface à partir de la page de lancement',
'control': 'Démarre l\'interface à partir de la page de contrôle',
'mediasrc1': 'adresse du contenu multimédia [aucune par défaut]',
'mediasrc2': 'adresse du contenu multimédia',
'mediasubsrc': 'adresse du contenu de sous-titres [aucun par défaut]',
'mediasublang': 'langue de sous-titres, . pour pas de sélection [fr,fre,fra,fr.* par défaut]',
'mediasublangcode': 'fr,fre,fra,fr.*',
'mediastartfrom': 'position temporelle de démarrage ou durée d\'affichage au format H:MM:SS [début/indéfinie par défaut]',
'slideshowduration': 'durée d\'affichage des images, si mediastrartfrom non défini, au format H:MM:SS [aucune par défaut]',
'endless': 'lecture en boucle [désactivé par défaut, toujours actif en mode lecture aléatoire de liste]',
'verbosity': 'niveau de verbosité de 0 à 2 [0 par défaut]',
'stopkey': 'Appuyez sur "S" pour stopper',
'auto': 'auto',
'sequential': 'séquentiel',
'random': 'aléatoire',
'remuxkey': 'Appuyez sur "!" et "M" pour alterner entre les modes de remuxage (MP4, MPEGTS, !MP4, !MPEGTS) pour la prochaine session de lecture - mode actuel: %s',
'servertypekey': 'Appuyez sur "T" pour alterner entre les types de serveur (auto, séquentiel, aléatoire) pour la prochaine session de lecture - mode actuel: %s',
'endlesskey': 'Appuyez sur "E" pour activer/désactiver la lecture en boucle - mode actuel: %s',
'enabled': 'activé',
'disabled': 'désactivé',
'remuxnext': 'Mode de remuxage pour la prochaine session de lecture: %s',
'servertypenext': 'Type de serveur pour la prochaine session de lecture: %s',
'endlessstatus': 'Lecture en boucle: %s'
}
}
EN_STRINGS = {
'mediaprovider': {
'opening': 'Opening of "%s" recognized as "%s" in "%s" mode - title: %s',
'extension': 'Extension of "%s" retained as "%s"',
'failure': 'Failure of the opening of "%s" as "%s"',
'subopening': 'Opening of the subtitles "%s" recognized as "%s"',
'subextension': 'Extension of the subtitles retained as "%s"',
'subfailure': 'Failure of the opening of the subtitles "%s" as "%s"',
'contentpath': 'content path',
'contenturl': 'content url',
'webpageurl': 'web page url',
'loadstart': 'Start of the loading in the content buffer',
'segmentbuffering': 'Segment %d -> placement in the zone %d of the buffer',
'segmentfailure': 'Segment %d -> failure of reading of the content',
'loadstop': 'End of the loading in the content buffer',
'loadinterrupt': 'Interruption of the loading in the content buffer',
'connection': 'Connection for delivery of "%s": persistent = %s - partial requests = %s',
'yes': 'yes',
'no': 'no',
'indexation': 'Indexation of the buffer on the connection %d',
'deindexation': 'Deindexation of the buffer',
'translation': 'Translation of the buffer to the position %d',
'present': 'Segment %d -> already present in the zone %d of the buffer'
},
'mediaserver': {
'connection': 'Connection to the delivery server of %s:%s',
'deliverystart': 'Connection %d -> start of the delivery of the content to %s:%s',
'delivery1': 'Connection %d -> segment %d -> delivery from the zone %d of the buffer',
'delivery2': 'Connection %d -> segment %d -> delivery',
'delivery3': 'Connection %d -> segment %d -> delivery from the buffer',
'exceeded': 'Connection %d -> segment %d -> the zone %d has been exceeded by the tail of the buffer',
'expulsion': 'Connection %d -> segment %d -> expulsion of the buffer',
'failure': 'Connection %d -> segment %d -> failure of the delivery of the content',
'deliveryfailure': 'Connection %d -> failure of the delivery of the content',
'deliverystop': 'Connection %d -> end of the delivery of the content',
'subdelivery': 'Delivery of the subtitles to %s:%s',
'subfailure': 'Failure of the delivery of the subtitles to %s:%s',
'start': 'Start, on the interface %s, of the delivery server in %s mode%s',
'sequential': 'sequential',
'random': 'random',
'unsupported': ' unsupported by the source',
'shutdown': 'Shutdown of the delivery server'
},
'dlnanotification': {
'start': 'Start of the server of listening of event notifications from DLNA %s at the address %s:%s',
'stop': 'Shutdown of the server of listening of event notifications from DLNA %s at the address %s:%s',
'alreadyactivated': 'Server of listening of event notifications from DLNA %s at the address %s:%s already activated',
'receipt': 'DLNA Renderer %s -> service %s -> receipt of the notification of event %s',
'notification': 'DLNA Renderer %s -> Service %s -> notification of event %s -> %s is changed to %s',
'alert': 'DLNA Renderer %s -> Service %s -> notification of event %s -> alerte: %s is changed to %s'
},
'dlnaadvertisement': {
'receipt': 'Receipt, on the interface %s, of an advertisement from the device %s (%s:%s): %s',
'ignored': 'Advertisement of the device %s (%s:%s) ignored due to the mismatch of the address of the description URL',
'set': 'Installation of the listening of advertisements of DLNA devices on the interface %s',
'fail': 'Failure of the installation of the listening of advertisements of DLNA devices on the interface %s',
'alreadyactivated': 'Listening of advertisements of DLNA devices already activated',
'start': 'Start of the server of listening of advertisements of DLNA devices',
'stop': 'Shutdown of the server of listening of advertisements of DLNA devices'
},
'dlnahandler': {
'ip_failure': 'Failure of the retrieval of the host ip address',
'registering': 'Registration of the %s %s on the interface %s',
'msearch1': 'Sending of a search message of uuid:%s',
'msearch2': 'Sending of a search message of DLNA device',
'msearch3': 'Sending of a search message of DNLA %s',
'sent': 'Sending of the search message on the interface %s',
'fail': 'Failure of the sending of the search message on the interface %s',
'receipt': 'Receipt, on the interface %s, of a response to the search message from %s:%s',
'ignored': 'Response from %s:%s ignored due to the mismatch of the address of the description URL',
'alreadyactivated': 'Search of DNLA %s already activated',
'start': 'Start of the search of DLNA %s',
'stop': 'End of the search of DLNA %s',
'commandabandonment': '%s %s -> service %s -> abandonment of the sending of the command %s',
'commandsending': '%s %s -> service %s -> sending of the command %s',
'commandfailure': '%s %s -> service %s -> failure of the sending of the command %s',
'commandsuccess': '%s %s -> service %s -> success of the sending of the command %s',
'responsefailure': '%s %s -> service %s -> failure of the processing of the response to the command %s',
'responsesuccess': '%s %s -> service %s -> success of the receipt of the response to the command %s',
'advertalreadyactivated': 'Listening of the advertisements of %s already activated',
'advertstart': 'Start of the listening of advertisements of %s',
'advertstop': 'End of the listening of advertisements of %s',
'subscralreadyactivated': 'Renderer %s -> service %s -> subscription to the events server already in place',
'subscrfailure': 'Renderer %s -> service %s -> failure of the request of subscription to the events server',
'subscrsuccess': 'Renderer %s -> service %s -> subscription to the events server under the SID %s for a period of %s',
'subscrrenewfailure': 'Renderer %s -> service %s -> failure of the request of renewal of subscription under SID %s to the events server',
'subscrrenewsuccess': 'Renderer %s -> service %s -> renewal of the subscription under SID %s to the events server for a period of %s',
'subscrunsubscrfailure': 'Renderer %s -> service %s -> failure of the request of end of subscription under SID %s to the events server',
'subscrunsubscrsuccess': 'Renderer %s -> service %s -> end of subscription under SID %s to the events server'
},
'websocket': {
'endacksuccess': 'WebSocket server %s:%s/%s -> WebSocket %s:%s -> success of the sending of the acknowledgment of receipt of the notice of end of connection',
'endackfailure': 'WebSocket server %s:%s/%s -> WebSocket %s:%s -> failure of the sending of the acknowledgment of receipt of the notice of end of connection',
'errorendnotification': 'WebSocket server %s:%s/%s -> WebSocket %s:%s -> sending of a notice of end of connection because of error %s',
'errorendnotificationsuccess': 'WebSocket server %s:%s/%s -> WebSocket %s:%s -> success of the sending of the notice of end of connection',
'errorendnotificationfailure': 'WebSocket server %s:%s/%s -> WebSocket %s:%s -> failure of the sending of the notice of end of connection',
'terminationdatasuccess': 'WebSocket server %s:%s/%s -> WebSocket %s:%s -> success of the sending of the termination data %s',
'terminationdatafailure': 'WebSocket server %s:%s/%s -> WebSocket %s:%s -> failure of the sending of the termination data %s',
'endnotificationsuccess': 'WebSocket server %s:%s/%s -> WebSocket %s:%s -> success of the sending of the notice of end of connection',
'endnotificationfailure': 'WebSocket server %s:%s/%s -> WebSocket %s:%s -> failure of the sending of the notice of end of connection',
'datasuccess': 'WebSocket server %s:%s/%s -> WebSocket %s:%s -> sending of the data %s',
'datafailure': 'WebSocket server %s:%s/%s -> WebSocket %s:%s -> failure of the sending of the data %s',
'datareceipt': 'WebSocket server %s:%s/%s -> WebSocket %s:%s -> receipt of the data %s',
'connectionrequest': 'WebSocket server %s:%s -> connection request from the WebSocket %s:%s',
'connectionrequestinvalid': 'WebSocket server %s:%s -> connection request from the WebSocket %s:%s invalid',
'connectionrequestnotfound': 'WebSocket server %s:%s -> path of the connection request from the WebSocket %s:%s not found: /%s',
'connectionresponsefailure': 'WebSocket server %s:%s/%s -> failure of the sending of the response to the connection request from the WebSocket %s:%s',
'connection': 'WebSocket server %s:%s/%s -> connection to the WebSocket %s:%s',
'endack': 'WebSocket server %s:%s/%s -> acknowledgment of end of connection from the WebSocket %s:%s',
'endnotification': 'WebSocket server %s:%s/%s -> notice of end of connection from the WebSocket %s:%s',
'connectionend': 'WebSocket server %s:%s/%s -> end of connection to the WebSocket %s:%s',
'start': 'Start of the server for Websocket at the address %s:%s',
'fail': 'Failure of the start of the server for Websocket at the address %s:%s',
'open': 'Websocket server %s:%s: opening of the channel /%s',
'close': 'Websocket server %s:%s: closure of the channel /%s',
'shutdown': 'Shutdonw of the server for Websocket at the address %s:%s'
},
'webinterface': {
'ip_failure': 'Failure of the retrieval of the host ip address',
'connection': 'Connection of the Web interface %s:%s',
'response': 'Response to the Web interface %s:%s - request: %s',
'formdatareceipt': 'Receipt of the form data %s from %s:%s',
'formdatareject': 'Rejection of the form data %s from %s:%s',
'playbackaccept': 'Acceptance of the playback request of %s%s starting at %s on %s from %s:%s',
'playbacksub': ' and %s',
'playbackreject': 'Rejection of the playback request of %s%s starting at %s on %s from %s:%s',
'rendererstart': 'Start of the display manager of renderer for Web interface server',
'launchrendererstart': 'Start of the display manager of renderer in the launch form for Web interface server',
'rendererstop': 'Shutdown of the display manager of renderer for Web interface server',
'controlstart': 'Start of the playback controller manager for Web interface server',
'controlinterrupt': 'Interruption of the playback controller manager for Web interface server',
'controlrenderer': 'Selection of the renderer %s on the interface %s',
'playlist': 'Playlist generated from the address %s: %s media contents',
'nocontent': 'Absence of media content under the address %s',
'nonegapless': 'Absence of support of gapless playback by the renderer %s',
'gapless': 'Gapless playback of the media contents from the address %s enabled',
'nogapless': 'Address %s incompatible with gapless playback',
'norendereranswer': 'Absence of response from the renderer %s',
'ready': 'Ready for playback of "%s"%s, by %s, on the renderer "%s"',
'subtitled': ', subtitled',
'direct': 'direct transmission of the address',
'random': 'delivery through server in random access mode',
'sequential': 'delivery through server in sequential access mode%s',
'remuxed': ', remuxed in %s',
'controlstop': 'Shutdown of the playback controller manager for Web interface server%s%s%s%s',
'status': ' - status ',
'start': 'Start of the Web interface server on the interface %s',
'alreadyrunning': 'Web interface server already in place',
'fail': 'Failure of the start of the Web interface server on the interface %s',
'shutdown': 'Shutdown of the Web interface server',
'jstart': 'Launch interface',
'jcontrol': 'Control interface',
'jrenderers': 'Renderers',
'jmwebsocketfailure': 'Failure of the establishment of the WebSocket connection',
'jmrenderersclosed': 'Renderers - interface closed',
'jmentervalidurl': 'Enter a valid media content URL',
'jmentervalidsuburl': 'Enter a valid subtitles URL',
'jmselectrenderer': 'First select a renderer',
'jplaybackposition': 'Playback position',
'jgoplay': 'Play',
'jreset': 'Reset',
'jplay': 'Play',
'jpause': 'Pause',
'jstop': 'Stop',
'jinterfaceclosed': 'interface closed',
'jback': 'back',
'jinitialization': 'initialization',
'jready': 'ready',
'jreadyfromstart': 'ready (playback from the beginning)',
'jinprogress': 'in progress',
'jinplayback': 'playback',
'jinpause': 'pause',
'jinstop': 'stop',
'jplaylist': 'Playlist',
'jurl': 'Media URL',
'jstatus': 'Status',
'jtargetposition': 'Target position',
'jmstop': 'Stop the playback ?'
},
'parser': {
'license': 'This program is licensed under the GNU GPLv3 copyleft license (see https://www.gnu.org/licenses)',
'help': 'display of the help message and interruption of the script',
'ip': 'IP address of the web interface [auto-selected by default - "0.0.0.0", meaning all interfaces, if option present with no address mention]',
'port': 'TCP port of the web interface [8000 by default]',
'hip': 'IP address of the DLNA controller/client [auto-selected by default - "0.0.0.0", meaning all interfaces, if option present with no address mention]',
'rendereruuid': 'uuid of the renderer [first renderer without selection on the uuid by default]',
'renderername': 'name of the renderer [first renderer without selection on the name by default]',
'servertype': 'type of server (a:auto, s:sequential, r:random, g:gapless/random, n:none) [a by default]',
'bufferquick': 'reduced size of buffer block for faster startup [disabled by default]',
'buffersize': 'size of the buffer in blocks of 1 MB [75 by default]',
'bufferahead': 'size of the sub-buffer of loading in advance in blocks of 1 MB [25 by default]',
'muxcontainer': 'type of remuxing container preceded by ! so that it is systematic [MP4 by default]',
'onreadyplay': 'direct playback as soon as the media content and the renderer are ready [disabled by default]',
'displayrenderers': 'Displays the renderers present on the network',
'start': 'Starts the interface from the launch page',
'control': 'Starts the interface from the control page',
'mediasrc1': 'adress of the multimedia content [none by default]',
'mediasrc2': 'adress of the multimedia content',
'mediasubsrc': 'adress of the subtitles content [none by default]',
'mediasublang': 'language of the subtitles, . for no selection [en,eng,en.* by default]',
'mediasublangcode': 'en,eng,en.*',
'mediastartfrom': 'start time position or display duration in format H:MM:SS [start/indefinite by default]',
'slideshowduration': 'display duration of the pictures, if mediastrartfrom not defined, in the format H:MM:SS [none by default]',
'endless': 'loop playback [disabled by default, always enabled in list random playback mode]',
'verbosity': 'verbosity level from 0 to 2 [0 by default]',
'stopkey': 'Press "S" to stop',
'auto': 'auto',
'sequential': 'sequential',
'random': 'random',
'remuxkey': 'Press "!" and "M" to switch between the remuxing modes (MP4, MPEGTS, !MP4, !MPEGTS) for the next playback session - current mode: %s',
'servertypekey': 'Press "T" to switch between the types of server (auto, sequential, random) for the next playback session - current mode: %s',
'endlesskey': 'Press "E" to enable/disable the loop playback - current mode: %s',
'enabled': 'enabled',
'disabled': 'disabled',
'remuxnext': 'Remuxing mode for the next playback session: %s',
'servertypenext': 'Type of server for the next playback session: %s',
'endlessstatus': 'Loop playback: %s'
}
}
LSTRINGS = EN_STRINGS
try:
if locale.getlocale()[0][:2].lower() == 'fr':
LSTRINGS = FR_STRINGS
except:
pass
class log_event:
def __init__(self, kmod, verbosity):
self.kmod = kmod
self.verbosity = verbosity
def log(self, level, kmsg, *var):
if level <= self.verbosity:
now = time.localtime()
s_now = '%02d/%02d/%04d %02d:%02d:%02d' % (now.tm_mday, now.tm_mon, now.tm_year, now.tm_hour, now.tm_min, now.tm_sec)
try:
print(s_now, ':', LSTRINGS[self.kmod][kmsg] % var)
except:
try:
print(s_now, ':', kmsg % var)
except:
print(s_now, ':', kmsg)
class ThreadedDualStackServer(socketserver.ThreadingMixIn, server.HTTPServer):
block_on_close = False
def __init__(self, *args, kmod, verbosity, auth_ip=None, **kwargs):
self.logger = log_event(kmod, verbosity)
if auth_ip:
if isinstance(auth_ip, tuple):
self.auth_ip = (*auth_ip, '127.0.0.1')
else:
self.auth_ip = (auth_ip, '127.0.0.1')
else:
self.auth_ip = None
server.HTTPServer.__init__(self, *args, **kwargs)
self.__dict__['_BaseServer__is_shut_down'].set()
def server_bind(self):
self.conn_sockets = []
try:
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
except:
pass
socketserver.TCPServer.server_bind(self)
self.server_name = '%s:%s' % self.server_address
self.server_port = self.server_address[1]
def process_request_thread(self, request, client_address):
self.conn_sockets.append(request)
self.logger.log(2, 'connection', *client_address)
super().process_request_thread(request, client_address)
def shutdown(self):
self.socket.close()
super().shutdown()
for sock in self.conn_sockets:
try:
sock.shutdown(socket.SHUT_RDWR)
except:
pass
def server_close(self):
pass
class MediaBuffer:
def __init__(self, BufferSize, BufferBlocSize):
self.content = [None] * BufferSize
self.bloc_size = BufferBlocSize
self.w_index = 0
self.r_indexes = []
self.create_lock = threading.Lock()
self.r_event = threading.Event()
self.w_condition = threading.Condition()
self.len = 0
self.t_index = None
class MediaProvider(threading.Thread):
STATUS_INITIALIZING = 0
STATUS_ABORTED = 1
STATUS_RUNNING = 2
STATUS_COMPLETED = 3
SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
SERVER_MODE_AUTO = 0
SERVER_MODE_SEQUENTIAL = 1
SERVER_MODE_RANDOM = 2
TITLE_MAX_LENGTH = 200
@classmethod
def open_url(cls, url, method=None):
header = {'User-Agent': 'Lavf'}
if method:
if method.upper() == 'HEAD':
header['Range'] = 'bytes=0-'
req = urllib.request.Request(url, headers=header, method=method)
rep = None
try:
rep = urllib.request.urlopen(req)
except urllib.error.HTTPError as e:
if e.code == HTTPStatus.NOT_ACCEPTABLE and method.upper() == 'HEAD':
del header['Range']
req = urllib.request.Request(url, headers=header, method=method)
rep = None
try:
rep = urllib.request.urlopen(req)
except:
pass
except:
pass
return rep
class HTTPConnectionTO(client.HTTPConnection):
def connect(self):
self.timeout = 1
try:
return super().connect()
finally:
self.timeout = None
try:
self.sock.settimeout(None)
except:
pass
class HTTPHandlerTO(urllib.request.HTTPHandler):
def http_open(self, req):
return self.do_open(MediaProvider.HTTPConnectionTO, req)
urlopento = urllib.request.build_opener(HTTPHandlerTO).open
def __init__(self, ServerMode, MediaSrc, MediaSrcType=None, MediaStartFrom=None, MediaBuffer=None, MediaBufferAhead=None, MediaMuxContainer=None, MediaSubSrc=None, MediaSubSrcType=None, MediaSubLang=None, MediaSubBuffer=None, MediaProcessProfile=None, FFmpegPort=None, BuildFinishedEvent=None, verbosity=0):
threading.Thread.__init__(self, daemon=True)
self.logger = log_event('mediaprovider', verbosity)
self.ServerMode = ServerMode if ServerMode in (MediaProvider.SERVER_MODE_SEQUENTIAL, MediaProvider.SERVER_MODE_RANDOM) else MediaProvider.SERVER_MODE_AUTO
self.MediaSrc = MediaSrc
self.MediaSrcType = MediaSrcType
self.MediaStartFrom = MediaStartFrom if not MediaStartFrom in (None, '0', '0:00', '00:00', '0:00:00', '00:00:00') else ''
self.MediaBuffer = MediaBuffer
if MediaBuffer:
self.MediaBufferSize = len(MediaBuffer.content)
self.MediaBufferAhead = MediaBufferAhead
else:
self.MediaBufferSize = 0
self.MediaBufferAhead = 0
if FFmpegPort:
if MediaMuxContainer:
self.MediaMuxAlways = MediaMuxContainer[0:1] == '!'
else:
self.MediaMuxAlways = None
self.MediaMuxContainer = None
if self.MediaMuxAlways:
self.MediaMuxContainer = MediaMuxContainer[1:]
else:
self.MediaMuxContainer = MediaMuxContainer
if self.MediaMuxContainer:
self.ffmpeg_server_url = 'http://127.0.0.1:%s' % FFmpegPort
if self.ServerMode == MediaProvider.SERVER_MODE_RANDOM:
self.MediaMuxAlways = None
self.MediaMuxContainer = None
else:
self.ffmpeg_server_url = ''
self.MediaMuxAlways = None
self.MediaMuxContainer = None
else:
self.ffmpeg_server_url = ''
self.MediaMuxAlways = None
self.MediaMuxContainer = None
if self.ServerMode == MediaProvider.SERVER_MODE_AUTO:
if self.MediaMuxAlways:
self.ServerMode = MediaProvider.SERVER_MODE_SEQUENTIAL
if not self.MediaMuxContainer:
self.ServerMode = MediaProvider.SERVER_MODE_RANDOM
self.FFmpeg_process = None
if MediaSubBuffer:
self.MediaSubBuffer = MediaSubBuffer
if (not MediaSubBuffer[1] if len(MediaSubBuffer) >= 2 else True):
if MediaSubSrc:
self.MediaSubSrc = MediaSubSrc
self.MediaSubSrcType = MediaSubSrcType
self.MediaSubLang = MediaSubLang
self.MediaSubBuffer.clear()
self.MediaSubBuffer.append(b'')
self.MediaSubBuffer.append('')
else:
self.MediaSubSrc = None
self.MediaSubSrcType = None
self.MediaSubLang = None
else:
self.MediaSubSrc = ''
else:
self.MediaSubSrc = None
self.MediaSubSrcType = None
self.MediaSubLang = None
self.FFmpeg_sub_process = None
self.MediaProcessProfile = MediaProcessProfile if MediaProcessProfile else ''
self.Connection = None
self.Persistent = None
self.AcceptRanges = None
self.MediaFeed = None
self.MediaFeedExt = None
self.MediaSize = None
self.MediaTitle = ''
self.Status = None
self.shutdown_requested = False
if isinstance(BuildFinishedEvent, threading.Event):
self.BuildFinishedEvent = BuildFinishedEvent
else:
self.BuildFinishedEvent = threading.Event()
def _open_FFmpeg(self, vid=None, aud=None, sub=None, in_sub_buffer=None, out_sub_buffer=None):
if not vid and not (sub and out_sub_buffer):
return None
ffmpeg_env = {'mediabuilder_address': '-' if sub else self.ffmpeg_server_url, 'mediabuilder_start': self.MediaStartFrom, 'mediabuilder_mux': self.MediaMuxContainer if vid else 'SRT', 'mediabuilder_profile': self.MediaProcessProfile}
ffmpeg_env['mediabuilder_vid'] = '"%s"' % (vid) if vid else ''
ffmpeg_env['mediabuilder_aud'] = '"%s"' % (aud) if aud else ''
ffmpeg_env['mediabuilder_sub'] = '-' if in_sub_buffer else ('"%s"' % (sub) if sub else '')
ffmpeg_env['mediabuilder_lang'] = '%s' % (self.MediaSubLang) if sub and self.MediaSubLang else ''
media_feed = None
while True:
if not sub:
self.FFmpeg_process = subprocess.Popen(r'"%s\%s"' % (MediaProvider.SCRIPT_PATH, 'ffmpeg.bat'), env={**os.environ,**ffmpeg_env}, creationflags=subprocess.CREATE_NEW_CONSOLE, startupinfo=subprocess.STARTUPINFO(dwFlags=subprocess.STARTF_USESHOWWINDOW, wShowWindow=6))
media_feed = None
while not media_feed and self.FFmpeg_process.poll() == None and not self.shutdown_requested:
try:
media_feed = MediaProvider.urlopento(self.ffmpeg_server_url)
except:
pass
if not self.shutdown_requested:
time.sleep(0.5)
if self.FFmpeg_process.poll() in (None, 0):
break
if media_feed:
try:
media_feed.close()
except:
pass
media_feed = None
else:
if self.ServerMode == MediaProvider.SERVER_MODE_RANDOM:
ffmpeg_env['mediabuilder_start'] = ''
sub_charenc = False
if in_sub_buffer:
try:
in_sub_buffer.decode('utf-8')
except:
sub_charenc = True
ffmpeg_env['mediabuilder_subcharenc'] = 'sub_charenc' if sub_charenc else ''
self.FFmpeg_sub_process = subprocess.Popen(r'"%s\%s"' % (MediaProvider.SCRIPT_PATH, 'ffmpeg.bat'), env={**os.environ,**ffmpeg_env}, stdin=None if not in_sub_buffer else subprocess.PIPE, stdout=subprocess.PIPE, creationflags=subprocess.CREATE_NEW_CONSOLE, startupinfo=subprocess.STARTUPINFO(dwFlags=subprocess.STARTF_USESHOWWINDOW, wShowWindow=6))
out_sub_buffer[0] = self.FFmpeg_sub_process.communicate(input=None if not in_sub_buffer else in_sub_buffer, timeout=30)[0]
if out_sub_buffer[0]:
out_sub_buffer[1] = '.srt'
break
if self.shutdown_requested:
break
if ffmpeg_env['mediabuilder_mux'][-2] != '-' :
ffmpeg_env['mediabuilder_mux'] = ffmpeg_env['mediabuilder_mux'] + '-'
else:
break
return media_feed
@classmethod
def parse_playlist(cls, src, check=True, stop=None, m3u_title=None):
if not mimetypes.inited:
mimetypes.init()
is_stop = lambda : False if stop is None else stop.is_set()
sh_str = lambda s: s if len(s) <= cls.TITLE_MAX_LENGTH else s[:cls.TITLE_MAX_LENGTH] + '…'
playlist = False
titles = []
if r'://' in src:
playlist = '?'
if '.' in src[-5:]:
if ''.join(src.rpartition('.')[1:3]).lower() in ('.m3u', '.m3u8'):
playlist = False
else:
media_mime = mimetypes.guess_type(src)[0]
if (media_mime or '')[0:5] in ('video', 'audio', 'image'):
playlist = False
if playlist:
playlist = '/playlist' in src.lower() or '/channels' in src.lower()
if playlist:
try:
process_result = subprocess.run(r'"%s\%s" %s' % (cls.SCRIPT_PATH, 'youtube-dl.bat', 'playlist'), env={**os.environ, 'mediabuilder_url': '"%s"' % src, 'mediabuilder_profile': ''}, capture_output=True)
if process_result.returncode == 0:
get_p_t = lambda j: (j['url'], j.get('title', j['url']))
try:
p_t = list(get_p_t(json.loads(e)) for e in process_result.stdout.splitlines() if not is_stop())
except:
return (False, []) if check else ([src], [sh_str(src if m3u_title is None else m3u_title)])
if not is_stop():
playlist = (e[0] for e in p_t)
titles = (sh_str(e[1]) for e in p_t)
if '//www.youtube' in src.lower():
playlist = (('https://www.youtube.com/watch?v=' if not 'http' in e.lower() else '') + e for e in playlist)
elif '//vimeo' in src.lower():
playlist = (('https://vimeo.com/' + ''.join(e.split('channels/')[1].split('/')[1:]) if 'channels/' in e else e) for e in playlist)
titles = (html.unescape(t) for t in titles)
if not is_stop():
playlist = list(playlist)
titles = list(titles)
else:
playlist = []
titles = []
else:
return (False, []) if check else ([src], [sh_str(src if m3u_title is None else m3u_title)])
except:
playlist = []
titles = []
return playlist, titles
else:
return (False, []) if check else ([src], [sh_str(src if m3u_title is None else m3u_title)])
elif os.path.isdir(src):
numb_exp = lambda t: '.'.join([(t[0].rstrip('0123456789') + t[0][len(t[0].rstrip('0123456789')):].rjust(5,'0')) if ('0' <= t[0][-1:] and t[0][-1:] <= '9') else t[0]] + t[1:2])
try:
dirs = list(f.path for f in os.scandir(src) if not is_stop() and f.is_dir())
dirs.sort(key=str.lower)
dirs.sort(key=lambda f: numb_exp(f.lower().rsplit('.', 1)))
if not is_stop():
playlist = list(f.path for f in os.scandir(src) if not is_stop() and f.is_file() and not ''.join(f.path.rpartition('.')[1:3]).lower() in ('.m3u', '.m3u8', '.wpl'))
playlist = list(e for e in playlist if not is_stop() and (mimetypes.guess_type(e)[0] or '')[0:5] in ('video', 'audio', 'image'))
if playlist and not is_stop():
playlist.sort(key=str.lower)
playlist.sort(key=lambda f: numb_exp(f.lower().rsplit('.', 1)))
if not is_stop():
playlist = playlist + list(e for p in (MediaProvider.parse_playlist(d, False, stop)[0] for d in dirs) if not is_stop() for e in p)
if is_stop():
playlist = []
except:
playlist = []
return playlist, list(map(sh_str, playlist))
elif '.' in src[-4:] and src.rsplit('.', 1)[-1].lower() == 'wpl':
try:
wpl = minidom.parse(src)
playlist = list((os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(src)), e.getAttribute('src'))) if not '://' in e.getAttribute('src') else e.getAttribute('src')) for e in wpl.getElementsByTagName('media') if not is_stop())
except:
playlist = []
if is_stop():
playlist = []
return playlist, list(map(sh_str, playlist))
elif '.' in src[-5:] and src.rsplit('.', 1)[-1].lower() in ('m3u8', 'm3u'):
playlist = []
try:
f = open(src, 'rt', encoding='utf-8' if src.rsplit('.', 1)[-1].lower() == 'm3u8' else None)
tit = None
for e in f.readlines():
if is_stop():
break
e = e.lstrip().rstrip('\r\n')
p_t = []
if e[:8].upper() == "#EXTINF:":
if ',' in e:
tit = e.rsplit(',', 1)[1].strip()
elif e and e[:1] != '#':
p_t_ = MediaProvider.parse_playlist(os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(src)), e)) if not '://' in e else e, False, stop, tit)
tit = None
playlist.extend(p_t_[0])
titles.extend(p_t_[1])
f.close()
except:
playlist = []
if is_stop():
playlist = []
return playlist, titles
else:
return (False, False) if check else ([src], [sh_str(src if m3u_title is None else m3u_title)])
@classmethod
def convert_to_smi(cls, MediaSubBuffer):
if not MediaSubBuffer:
return None
if not MediaSubBuffer[0]:
return None
if len(MediaSubBuffer) >= 4:
if MediaSubBuffer[3] == '.smi':
if MediaSubBuffer[2]:
return True
else:
return None
else:
MediaSubBuffer[2] = None
MediaSubBuffer[3] = '.smi'
else:
MediaSubBuffer.extend([None] * (4 - len(MediaSubBuffer)))
MediaSubBuffer[3] = '.smi'
ffmpeg_env = {'mediabuilder_address': '-', 'mediabuilder_start': '', 'mediabuilder_mux': 'SRT', 'mediabuilder_profile': ''}
ffmpeg_env['mediabuilder_sub'] = '-'
ffmpeg_env['mediabuilder_lang'] = ''
sub_charenc = False
try:
MediaSubBuffer[0].decode('utf-8')
except:
sub_charenc = True
try:
ffmpeg_env['mediabuilder_subcharenc'] = 'sub_charenc' if sub_charenc else ''
FFmpeg_sub_process = subprocess.Popen(r'"%s\%s"' % (MediaProvider.SCRIPT_PATH, 'ffmpeg.bat'), env={**os.environ,**ffmpeg_env}, stdin=subprocess.PIPE, stdout=subprocess.PIPE, creationflags=subprocess.CREATE_NEW_CONSOLE, startupinfo=subprocess.STARTUPINFO(dwFlags=subprocess.STARTF_USESHOWWINDOW, wShowWindow=6))
srt_sub_buffer = FFmpeg_sub_process.communicate(input=MediaSubBuffer[0], timeout=30)[0].decode('utf-8')
if not srt_sub_buffer:
return None
except:
return None
smi_sub_buffer = \
'<SAMI>\r\n' \
'<HEAD>\r\n' \
' <STYLE TYPE="text/css">\r\n' \
' <!--\r\n' \
' P {\r\n' \
' background-color: black;\r\n' \
' color: white;\r\n' \
' margin-left: 1pt;\r\n' \
' margin-right: 1pt;\r\n' \
' margin-bottom: 2pt;\r\n' \
' margin-top: 2pt;\r\n' \
' text-align: center;\r\n' \
' font-size: 16pt;\r\n' \
' font-family: arial;\r\n' \
' font-weight: bold;\r\n' \
' }\r\n' \
' .CC {Name:default; lang: default;}\r\n' \
' -->\r\n' \
' </STYLE>\r\n' \
'</HEAD>\r\n' \
'<BODY>\r\n'
bloc_ln = 0
was_bl = True
d_ms = 0
f_ms = 0
try:
for ln in srt_sub_buffer.splitlines():
l = ln.rstrip('\r\n')
if l == '':
was_bl = True
if bloc_ln >= 2:
smi_sub_buffer = smi_sub_buffer + '<br>'
continue
if bloc_ln == 0:
if not l.strip().isdecimal():
return None
bloc_ln += 1
was_bl = False
elif was_bl and l.strip().isdecimal() and bloc_ln >= 2:
bloc_ln = 1
was_bl = False
while smi_sub_buffer[-4:] == '<br>':
smi_sub_buffer = smi_sub_buffer[:-4]
smi_sub_buffer = smi_sub_buffer + '</SYNC>\r\n'
smi_sub_buffer = smi_sub_buffer + '<SYNC start="%d"><P Class="CC"> </SYNC>\r\n' % f_ms
elif bloc_ln == 1:
d, f = l.split('-->')
d = d.strip()
f = f.strip()
if ',' in d:
d, d_ms = d.rsplit(',')
d_ms = int(d_ms)
else:
d_ms = 0
if ',' in f:
f, f_ms = f.rsplit(',')
f_ms = int(f_ms)
else:
f_ms = 0
d_ms += sum(int(t[0])*t[1] for t in zip(reversed(d.split(':')), [1,60,3600])) * 1000
f_ms += sum(int(t[0])*t[1] for t in zip(reversed(f.split(':')), [1,60,3600])) * 1000
if d_ms > 0 and smi_sub_buffer[-8:-2] == '<BODY>':
smi_sub_buffer = smi_sub_buffer + '<SYNC start="0"><P Class="CC"> </SYNC>\r\n'
smi_sub_buffer = smi_sub_buffer + '<SYNC start="%d"><P Class="CC">' % d_ms
bloc_ln += 1
was_bl = False
elif bloc_ln >= 2:
smi_sub_buffer = smi_sub_buffer + l + '<br>'
bloc_ln += 1
was_bl = False
while smi_sub_buffer[-4:] == '<br>':
smi_sub_buffer = smi_sub_buffer[:-4]
if '<SYNC' in smi_sub_buffer:
smi_sub_buffer = smi_sub_buffer + '</SYNC>\r\n'
smi_sub_buffer = smi_sub_buffer + '<SYNC start="%d"><P Class="CC"> </SYNC>\r\n' % f_ms
smi_sub_buffer = smi_sub_buffer + '</BODY>\r\n</SAMI>'
except:
return None
MediaSubBuffer[2] = smi_sub_buffer.encode('ansi')
return True
def MediaBuilder(self):
if not self.MediaBuffer and self.ServerMode in (MediaProvider.SERVER_MODE_AUTO, MediaProvider.SERVER_MODE_SEQUENTIAL):
return False
if not mimetypes.inited:
mimetypes.init()
if not self.MediaSrcType:
if r'://' in self.MediaSrc:
self.MediaSrcType = 'WebPageURL'
if '.' in self.MediaSrc[-5:]:
media_mime = mimetypes.guess_type(self.MediaSrc)[0]
if media_mime:
if media_mime[0:5] in ('video', 'audio', 'image'):
self.MediaSrcType = 'ContentURL'
else:
self.MediaSrcType = 'ContentPath'
if self.MediaSubSrc and not self.MediaSubSrcType:
sub_ext = (''.join(self.MediaSubSrc.rstrip().rpartition('.')[-2:])).lower() if '.' in self.MediaSubSrc else ''
if r'://' in self.MediaSubSrc:
if '?' in sub_ext:
sub_ext = sub_ext.rpartition('?')[0]
if sub_ext in ('.ttxt', '.txt', '.smi', '.srt', '.sub', '.ssa', '.ass', '.vtt', '.m3u8'):
self.MediaSubSrcType = 'ContentURL'
else:
self.MediaSubSrcType = 'WebPageURL'
else:
if os.path.isdir(self.MediaSubSrc):
for sub_ext in ('.ttxt', '.txt', '.smi', '.srt', '.sub', '.ssa', '.ass', '.vtt'):
if os.path.exists(os.path.join(self.MediaSubSrc, os.path.splitext(os.path.basename(self.MediaSrc))[0]) + sub_ext):
self.MediaSubSrc = os.path.join(self.MediaSubSrc, os.path.splitext(os.path.basename(self.MediaSrc))[0]) + sub_ext
break
if os.path.isdir(self.MediaSubSrc) and os.path.exists(os.path.join(self.MediaSubSrc, os.path.basename(self.MediaSrc))):
self.MediaSubSrc = os.path.join(self.MediaSubSrc, os.path.basename(self.MediaSrc))
self.MediaSubSrcType = 'ContentPath'
if self.MediaSrcType.lower() == 'ContentPath'.lower() and not self.MediaMuxAlways and self.ServerMode != MediaProvider.SERVER_MODE_SEQUENTIAL and self.MediaSubSrc == None and self.MediaSubBuffer:
for sub_ext in ('.ttxt', '.txt', '.smi', '.srt', '.sub', '.ssa', '.ass', '.vtt'):
if os.path.exists(os.path.splitext(self.MediaSrc)[0] + sub_ext):
self.MediaSubSrc = os.path.splitext(self.MediaSrc)[0] + sub_ext
self.MediaSubSrcType = 'ContentPath'
break