/
jamu.py
executable file
·6383 lines (5860 loc) · 324 KB
/
jamu.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
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# ----------------------
# Name: jamu.py Just.Another.Metadata.Utility
# Python Script
# Author: R.D. Vaughan
# Purpose: This python script is intended to perform a variety of utility functions on mythvideo
# metadata and the associated video files.
#
# The primary movie source for graphics and data is themoviedb.com wiki.
# The primary TV Series source for graphics and data is thetvdb.com wiki.
# Users of this script are encouraged to populate both themoviedb.com and thetvdb.com
# with posters, fan art and banners and meta data. The richer the source the more valuable
# the script.
# This script uses the python module tvdb_api.py (v0.6DEV or higher) found at
# http://pypi.python.org/pypi?%3Aaction=search&term=tvnamer&submit=search thanks
# to the authors of this excellent module.
# The tvdb_api.py module uses the full access XML api published by thetvdb.com see:
# http://thetvdb.com/wiki/index.php?title=Programmers_API
# This python script's functionality is enhanced if you have installed "tvnamer.py" created by
# "dbr/Ben" who is also the author of the "tvdb_api.py" module.
# "tvnamer.py" is used to rename avi files with series/episode information found at
# thetvdb.com
# Python access to the tmdb api started with a module from dbr/Ben and then enhanced for
# Jamu's needs.
# The routines to select video files was copied and modified from tvnamer.py mentioned above.
# The routine "_save_video_metadata_to_mythdb" has been taken and modified from
# "find_meta.py" author Pekka Jääskeläinen.
# The routine "_addCastGenre" was taken and modified from "tvdb-bulk-update.py" by
# author David Shilvock <davels@telus.net>.
#
# Command line examples:
# See help (-u and -h) options
#
# License:Creative Commons GNU GPL v2
# (http://creativecommons.org/licenses/GPL/2.0/)
#-------------------------------------
__title__ ="JAMU - Just.Another.Metadata.Utility";
__author__="R.D.Vaughan"
__purpose__='''
This python script is intended to perform a variety of utility functions on mythvideo metadata
and the associated video files.
The primary movie source for graphics and data is themoviedb.com wiki.
The primary TV Series source for graphics and data is thetvdb.com wiki.
Users of this script are encouraged to populate both themoviedb.com and thetvdb.com with posters,
fan art and banners and meta data. The richer the source the more valuable the script.
'''
__version__=u"v0.8.0"
# 0.1.0 Initial development
# 0.2.0 Inital beta release
# 0.3.0 Add mythvideo metadata updating including movie graphics through
# the use of tmdb.pl when the perl script exists
# 0.3.1 Add mythvideo meta data add and update functionality. Intend use for
# maintenance cron jobs.
# Increase integration with mythtvideo download meta data and MythUI
# Added the ability to movie video files while maintaining the metadata
# 0.3.2 Fixed bug where some poster downloads were unnecessary
# Fixed bug where the mythtv database was updated for no reason
# Fixed bug in jamu-example.conf "min_poster_size" variable had '=' not ':'
# Fixed bug where a unicode URL would abort the script
# Using ffmpeg added setting accurate video length in minutes. A hack but
# lacked python method to find audio/video properties.
# 0.3.3 Add logic to skip any video with a inetref of '99999999'. Meta data and
# graphics are all manually entered and should not be altered by Jamu.
# Currently used for any meta data that you do not want modified by Jamu.
# Fixed issues with filenames containing Unicode characters.
# 0.3.4 Added logic to skip any secondary source meta data plot less than 10 words.
# Properly initialized a new record so warning messages do not display.
# In plot meta data replace line-feeds with a space (e.g. Space Cowboys
# plot contains line-feeds). Mythvideo does not expect line-feeds in a plot.
# Significant improvements in combining meta data between primary and
# secondary data sources.
# Remove 'tmdb.pl' calls and use the tmdb api directly.
# Added detection of broken symbolic links and fixed those links.
# Fixed inconsistencies in graphics file extentions (as received from the
# sources), made all extentions lowercase and changed ".jpeg" to ".jpg".
# 0.3.5 Fixed bug when themoviedb.com times out from an api request.
# A few documentation corrections.
# Fixed a bug with utf8 directory names.
# Added code to not abort script when themoviedb.com has problems. The issue
# is reported but the scripts continues processing.
# Added option "-W" to download graphics for Scheduled and Recorded videos.
# Change the "-J" Janitor function to avoid deleting graphics for Scheduled
# and Recorded videos.
# Fixed bug where a TMDB Poster image was not found when it was really
# available.
# 0.3.6 Fixed bug when searching themoviedb.com for a movie by title or
# alternate title.
# Increased accuracy of non-interactive TMDB movie searching and matching.
# Set up for transition to TMDB's beta v2.1 api which adds language support.
# Corrected Watched Recording graphic file naming convention for movies.
# If interactive mode is selected but an exact match is found for a movie
# then the exact match is chosen and no interative session is initiated.
# Added additional messages when access to MythTV python bindings has issues.
# 0.3.7 Removed some redundant code.
# Sync up with v1.0 of tvdb_api and new way to assign tvdb api key
# Added an option (-MG) to allow Jamu best guessing at a video's inetref
# number. To guess accurately the video file name must be very close to
# those found on tmdb or imdb and tvdb web sites.
# Remove all use of the MythVideo.py "pruneMetadata" routine as it deletes
# records from the Mythvideo table for all video files with relative file
# paths.
# Jamu will skip processing any videometadata which is using a Storage group.
# Jamu will now restrict itself to updating only videometadata records whose
# video files reside on the current host machine. In the case where a user
# has multiple backends jamu must run on each of those backends.
# The Janitor option (-MJ) now checks if the users has set the plugins
# MythGallery, MythGame and MythMusic to use the same graphics directories as
# MythVideo. If they share directories the Janitor option will exit
# without removing any graphics files. Messages indicating which directories
# are in conflict will be displayed.
# Added the detection of video or graphics on an NFS mount exiting jamu without
# any processing and displaying a message why this has been done. A new option
# for NFS (-MN) will allow a user to override this check and jamu will continue
# processing.
# Fixed a bug when TMDB does not have a 'year' for a movie (e.g. 'Bambi')
# Added compatibility with or without the MythTV.py Ticket #6678
# Fixed a bug when ffmpeg cannot find the true length in minutes of a video
# Cleaned up documenation consistency with Warning and Error messages.
# Added to the existing TV episode video file renaming (-MF) option.
# Now movie video files can also be renamed to the format "title (year)"
# e.g. "The Duchess (2008)". If tmdb.com has no year for the movie then only
# the movie title will be used when renaming. Any existing metadata is
# preserved.
# 0.3.8 Made changes to sync up with MythTV trunk change set [r21138].
# Now handles TVDB's change from a 5 digit inetref number to 6 digits.
# 0.3.9 Check accessability (Read and Write) to directories and files before
# including them in files/directories to process.
# Add the ability to process Storage Groups for all Videos and graphics.
# Jamu now uses MythVideo.py binding's Genre and Cast routines
# Fixed a unicode bug with file paths.
# Fixed a unicode bug with some URLs containing UTF8 characters.
# Fixed a bug were a bad image file could avbort the script.
# Changed all subdirectory cover art to a copied graphic file "folder.jpg/png"
# to conform to the Storage Group standard. This also works for local subdirs.
# Fixed a bug where a TV series with out a season specific poster or
# banner would get repeatedly download.
# 0.4.0 Removed a few lines of debugging code which should never have been left in a
# distrubuted version.
# Fixed the check that confirms that all Video and graphic directories are
# read and writable.
# Fixed a bug where under rare circumstances a graphic would be repeatedly
# downloaded.
# Made the installation of the python IMDbPy library manditory.
# For all movies IMDB numbers will be used instead of converting to TMDB
# numbers. This is done to maintain consistency with MythVideo movie inetref
# numbers.
# 0.4.1 Fixed an obscure video file rename (-F option) error
# 0.4.2 Fixed a bug where bad data for either TMDB or TVDB would abort script
# 0.4.3 Recent changes in the MythVideo UI graphic hunts (cover art and fanart)
# have made Jamu's creation of "folder.xxx" graphics redundant. This
# feature has been turned off in Jamu. There is a new user option
# "folderart" that can reactivate this feature through the Jamu
# configuration file.
# 0.4.4 Changes to assist SG image hunting Jamu now adds the suffix "_coverart,
# fanart, _banner, _screenshot" respectively to downloaded graphics.
# With the use of a graphic suffix the requirement for unique graphics
# directories is gone. The check has been removed.
# 0.4.5 Fixed a bug where lowercase tv video filenames caused graphics files to
# also be lowercase which can cause graphics to be downloaded twice.
# Fixed a bug in graphics file name creation for a TV season.
# Added checks for compatible python library versions of xml and MySQLdb
# 0.4.6 Fixed a bug where a bad IMDB number in TMDB caused an abort.
# 0.4.7 Fixed a bug where a 'recordedprogram' record is not properly paired with a
# 'recorded' record. This results in no "airdate" information being available
# and a script abort. An airdate year of u'0000' will be assumed.
# Fix an abort bug when IMDB is having service problems and a list of
# movies cannot be retrieved.
# 0.4.8 Fixed a bug in a -MJ option check that removing graphics would not
# conflict with graphic directories for non-Mythvideo plugins.
# 0.4.9 Combine the video file extentions found in the "videotypes" table with those
# in Jamu to avoid possible issues in the (-MJ) option and to have tighter
# integration with MythVideo user file extention settings.
# 0.5.0 Fixed a bug where a filename containing invalid characters caused an abort.
# Such invalid filenames are now skipped with an appropriate message.
# Added to the -MW option the fetching of graphics from TVDB and TMDB for
# videos added by Miro Bridge to either Watched Recordings or MythVideo.
# If Miro Bridge is not being used no additional processing is performed.
# Two new sections ([mb_tv] and [mb_movies]) were added to the Jamu
# configuration file to accomodate this new functionality.
# The jamu configuration file now has a default name and location of
# "~/.mythtv/jamu.conf". This can be overridden with the command line option.
# This has been done so Jamu can better support Mythbuntu.
# Removed code that was required until ticket #6678 was committed with
# change set [21191]
# Filtered out checks for video run length on iso, img ... etc potentially
# large video files due to processing overhead especially on NFS mounts.
# With the -MW option skip any recordings who's recgroup is "Deleted"
# Fixed an abort where a TVDB TV series exists for a language but does not
# have a series name in other languages.
# 0.5.1 Fixed an abort when a user specifies secondary source input parameters
# that cannot be parsed from the file name. This
# covers secondary sources for metadata and graphics.
# Fixed an abort when thetvdb.com cannot be contact due to network or
# site issues.
# Added detection of erroneous graphics file downloads that are actually HTML
# due to source Web site issues. Jamu's (-MJ) janitor option also detects,
# deletes these files and repairs the MythVideo record if necessary.
# For the -MW option any downloaded graphics names will use the title of the
# recorded program instead of that found on sources like TVDB and TMDB. This
# resolves Watch Recordings image hunt issues when Schedule Direct uses a
# different program title then is on either TVDB or TMDB.
# Fixed an obscure bug where TVDB returns empty graphics URLs along with
# proper URLs. The empty URLs are now ignored.
# Fixed a bug when a language was specified and there were no graphics
# for the specified language none were returned/downloaded. This even when
# graphics for other languages were available. Now if there are no selected
# language graphics English graphics are the fall back and if there are no
# English graphics then any available graphics will be returned.
# 0.5.2 Fixed an abort when trying to add a storage group graphics without a
# proper file path.
# 0.5.3 Fixed a bug where the filemarkup table is not cleaned up if Jamu renames
# a Miro movie trailer video file that the user wants to keep in MythVideo.
# Fixed a bug with Miro video file renaming of Miro Movie trailers
# for the same movie but which had different file extentions.
# 0.5.4 Conform to changeset 22104 setting of SG graphics directories to default to SG Videos if not configured.
# 0.5.5 Deal with TV Series and Movie titles with a "/" forward slash in their name e.g. "Face/Off"
# 0.5.6 Correct an issue when a user has a mixture of local and SG video records in MythVideo. Jamu was
# adding a hostname when the video had an absolute path. This caused issues with playback.
# Added more informative error messages when TMDB is returning bad xml responses.
# Fixed an error in the graphic file naming convention when graphics share the same download directory.
# 0.5.7 Remove the override of the TVDB graphics URL to the mirror site. See Kobe's comment:
# http://forums.thetvdb.com/viewtopic.php?f=4&t=2161#p9089
# 0.5.8 The issue fixed in v0.5.5 with invalid file name creation did not fully cover TV shows It does now.
# 0.5.9 Changed permissions checks on video directories to only require RW for the destination directories
# involved in the move. With this change if a user requested a file rename (-F) option and the video
# file does not have RW access the rename will be ignored.
# Uses that have their Video directories set to access and read-only can now use Jamu.
# Added a stdout display of the directories that Jamu will use for processing. This information may help
# users resolve issues. The display happens ONLY when the -V (verbose) option is used.
# 0.6.0 Changed The Janitor -J option to deal with graphics associated with a VIDEO_TS directory.
# Stopped Jamu from processing any files in a "VIDEO_TS" directory as it was leading to multiple
# MythVideo entires for *.VOB files. Jamu does not process multi-part videos.
# Added the use of PID files to prevent two instances of the same Jamu -M options running at the same
# time. This deals with issues when a meta data source is off line for an extended
# period of time and Jamu runs as a cronjob. Options effected are (-M, -MW and -MG).
# Change to have jamu use the TMDB Movie title as is done in MythVideo rather than the file name.
# Fixed a bug when TMDB genres are filtered and none remain they were still being added. This bug was
# spotted and correct by Mathieu Brabant (thanks).
# Added the ability for a user to filter additional characters from a file name this is important for
# users using MS-Windows file systems as a CIFS mount.
# 0.6.1 Added directory name parsing support for TV series title, series number and episode numbers. Patch
# contributed by Mitko Haralanov (thanks).
# 0.6.2 Added updating the 'homepage', 'releasedata' and 'hash' fields in the videometadata table
# is the field exists. These fields is only present in trunk.
# Properly initalize the homepage, hash, releasedate fields when adding a new videometadata record.
# 0.6.3 Convert to new python bindings and replace all direct mysql data base calls. See ticket #7264
# Remove the requirement for the MySQLdb python library.
# Removed the folder icon symlink code as it is redundant with MythVideo internal functionality.
# The 'folderart' option is no longer support on a jamu.conf file and will be ignored if present.
# Fixed a bug where a FE video directory was set but there were no FE image directories.
# If there were local SG images directories set they were being used. This is an invalid
# configuration that should have caused an error.
# 0.6.4 Added a new option (-R) to allow just interactively populate the video reference numbers from
# TVDB and TMDB without any meta data downloads. After that the user runs Jamu with the -M option
# and the meta data and images are downloaded.
# Added to the interactive interface the ability to select a reference number of '99999999'
# which effectively tells jamu to ignore the specific video from all further processing.
# Changed the return code from 1 to 0 when Jamu exits without processing if there is already an
# instance of Jamu running. This was causing issues for Mythbuntu when TVDB or TMDB was down for an
# extended period.
# Added a new jamu.conf section [ignore-directory] to list Video sub-directories that Jamu should
# ignore.
# Change Jamu's import of tvdb and tmdb api libraries to use the installed versions found with the
# MythTV python bindings.
# Changed Jamu to use the tmdb API v2.0 python library
# Jamu will always use the TMDB reference number over the IMDB number but still supports IMDB#s
# Jamu interactive sessions for movies now lists the TMDB#s instead of IMDB#s
# Jamu will convert any IMDB#s to TMDB#s when themoviedb.org includes that movie. This is in line
# with MythVideo changes. Graphics for the movie will also be renamed so they do not need to be
# re-downloaded.
# Add the production countries for movies when TMDB provides that information.
# Adjusted the -MW option to add a " Season 1" to any downloaded image filename for TV shows.
# This must be done to make sure that TV shows like "24" do not clash with a movie's TMDB# like
# (Kill Bill Vol.1) which is "24".
# Added message display for exceptions where the message may enhance problem analysis.
# Removed logic which checked that a TV episode was using Season graphics rather than Series graphics.
# Unfortunately there was a chance that the a Series's graphics could clobber a movie with the same TMDB#
# as the series title (e.g. the movie Kill Bill Vol.1 and the TV series 24). A positive is that a number
# of redundant TV Series images can be removed through the jamu -MJ option.
# Improved the -MW options detection of TV series when the EPG data does not include a subtitle. Users
# can add the specific TVDB numbers to the 'series_name_override' section of the jamu.conf file.
# Australian users had mentioned this as an issue, previously the TV series was always being mistaken
# for a movie.
# Jamu will now download the top rated TV Series season coverart and banner images. This enhancement
# matches MythVideo processing.
# 0.6.5 Small fix related to the bindings changes.
# 0.6.6 Fixed Exception messages
# Change all occurances of 'mythbeconn.host' to 'mythbeconn.hostname' to be consistent with bindings
# 0.6.7 Fixed the (-J) janitor option from removing the Mirobridge default images when they are not being used
# 0.6.8 Fixed a (-J) janitor option statistics error due to skipping Mirbridge default images
# 0.6.9 Fixed an abort when IMDBpy returns movie matches with incomplete data
# Fixed an abort where an IMDB# was being used instead of a TMDB#
# Fixed an abort when a storage directory name caused an UnicodeEncodeError or TypeError exception
# 0.7.0 Fixed an (-MW) option abort when a recorded program or upcoming program did not have a title
# 0.7.1 Fixed a bug where movies with punctutation ("Mr. Magoo") were not finding matches
# Fixed bug with interactive mode when a user enters a reference number directly rather than
# making a list selection
# These bugs were both identified by Edi Iten (thanks)
# 0.7.2 Fixed a bug where an inetref field was not properly initialized and caused an abort. Ticket #8243
# 0.7.3 Fixed a bug where a user selected TMDB# was not being used.
# Minor change to fuzzy matching of a file named parsed title with those from TMDB and TVDB.
# 0.7.4 Update for changes in Python bindings
# 0.7.5 Added the TMDB MovieRating as videometadata table "rating" field
# 0.7.6 Modifications to support MythTV python bindings changes
# 0.7.7 Pull hostname from python bindings instead of socket libraries
# 0.7.8 Replace uses of MythVideo.getVideo()
# 0.7.9 Deal with jamu.conf entries that have unicode characters
# Replace 'xml' module version check with generic Python version, to correct failure in Python 2.7
# 0.8.0 Fixed a bug which caused jamu to crash due to an extra unicode conversion introduced in 0.7.9.
# See also #9637.
usage_txt=u'''
JAMU - Just.Another.Metadata.Utility is a versatile utility for downloading graphics and meta data
for both movies and TV Series information from themoviedb.com wiki and thetvdb.com wiki. In addition
the MythTV data base is updated with the downloaded information.
Here are the main uses for this utility:
MythTV users should review the Jamu wiki page at http://www.mythtv.org/wiki/Jamu for details.
1) Simple command line invocation to display or download data from thetvdb.com.
Data can be one or more of: Posters/Cover art, Banners, Fan art,
Episode Images and Episode meta data. use the command "jamu -e | less" to see
command line examples.
2) Mass downloads of data matching your video files. **
This typically done once to download the information for your video collection.
3) Automated maintenance of the information in your video collection. **
4) The creation of video file names which can be used to set the file name of your recorded TV shows.
File names can be formated to the users preference with information like series name, season number,
episode number and episode name. MythTV users may find this valuable as part of a user job
that is spawned automatically by mythbackend when recording is finished.
5) Jamu's modules can be imported into your own python scripts to create enhanced functionality.
6) With the installation of free ImageMagick's utilities (specifically 'mogrify') you can resize
graphics when they are downloaded.
7) Update the MythTV data base with links to posters, banners, fanart and episode images and optionally
download missing graphics if they exist. This feature can be used for mass updates and regular
maintenance.
'''
examples_txt=u'''
MythTV users should review the Jamu wiki page at http://www.mythtv.org/wiki/Jamu for details.
These examples are primarily for non-MythTV users of Jamu.
jamu command line examples:
NOTE: Included here are simple examples of jamu in action.
Please review jamu_README for advise on how to get the most out of jamu.
( Display a TV series top rated poster fanart and banner URLs)
> jamu -tS PBF "Sanctuary"
poster:http://www.thetvdb.com/banners/posters/80159-1.jpg
fanart:http://www.thetvdb.com/banners/fanart/original/80159-2.jpg
banner:http://www.thetvdb.com/banners/graphical/80159-g2.jpg
( Display the URL for a TV series episode )
> jamu -tS I "Fringe" 1 5
filename:http://www.thetvdb.com/banners/episodes/82066-391049.jpg
( Display poster, fanart and banner graphics for a TV series but limited to two per type in a season )
> jamu -S PBF -m 2 "24" 4
poster:http://www.thetvdb.com/banners/seasons/76290-4-3.jpg
poster:http://www.thetvdb.com/banners/seasons/76290-4.jpg
fanart:http://www.thetvdb.com/banners/fanart/original/76290-1.jpg
fanart:http://www.thetvdb.com/banners/fanart/original/76290-2.jpg
banner:http://www.thetvdb.com/banners/seasonswide/76290-4.jpg
banner:http://www.thetvdb.com/banners/seasonswide/76290-4-3.jpg
( Display a file name string (less file extention and directory path) for a TV episode )
> jamu -F "24" 4 3
24 - S04E03 - Day 4: 9:00 A.M.-10:00 A.M.
> jamu -F "24" "Day 4: 9:00 A.M.-10:00 A.M."
24 - S04E03 - Day 4: 9:00 A.M.-10:00 A.M.
( Using SID number instead of series name )
> jamu -F 76290 4 3
24 - S04E03 - Day 4: 9:00 A.M.-10:00 A.M.
( Simulate a dry run for the download of a TV series top rated poster and fanart )
> jamu -sdtS PF "Fringe"
Simulation download of URL(http://www.thetvdb.com/banners/posters/82066-6.jpg) to File(~/Pictures/Poster - 82066-6.jpg)
Get_Poster downloading successfully processed
Simulation download of URL(http://www.thetvdb.com/banners/fanart/original/82066-11.jpg) to File(~/Pictures/Fanart - 82066-11.jpg)
Get_Fanart downloading successfully processed
( Download the Episode meta data and episode image for a video file whose file name contains the series and season/episode numbers)
> jamu -dS EI "~/Pictures/Fringe - S01E01.mkv"
Episode meta data and/or images downloads successfully processed
> ls -ls
total 2
60 -rw-r--r-- 1 user user 53567 2009-03-12 22:05 Fringe - S01E01 - Pilot.jpg
4 -rw-r--r-- 1 user user 1059 2009-03-12 22:05 Fringe - S01E01 - Pilot.meta
4 -rw-r--r-- 1 user user 811 2009-03-12 13:22 Fringe - S01E01.mkv
( Display Episode meta data for a TV series )
> jamu -S E "24" 5 3
series:24
seasonnumber:5
episodenumber:3
episodename:Day 5: 9:00 A.M.-10:00 A.M.
rating:None
overview:Jack conceals himself inside the airport hanger and surveys the Russian separatists, feeding information to Curtis and his assault team.
The terrorists begin executing hostages in an attempt to make Logan cave into their demands.
Martha discovers that all traces of her conversation with Palmer may not have been erased.
director:Brad Turner
writer:Manny Coto
gueststars:John Gleeson Connolly, V.J. Foster, David Dayan Fisher, Taylor Nichols, Steve Edwards, Taras Los, Joey Munguia, Reggie Jordan, Lou Richards, Karla Zamudio
imdb_id:None
filename:http://www.thetvdb.com/banners/episodes/76290-306117.jpg
epimgflag:None
language:en
firstaired:2006-01-16
lastupdated:1197942225
productioncode:5AFF03
id:306117
seriesid:76290
seasonid:10067
absolute_number:None
combined_season:5
combined_episodenumber:4.0
dvd_season:5
dvd_discid:None
dvd_chapter:None
dvd_episodenumber:4.0
( Specify a user defined configuration file to set most of the configuration variables )
> jamu -C "~/.jamu/jamu.conf" -S P "Supernatural"
poster:http://www.thetvdb.com/banners/posters/78901-3.jpg
poster:http://www.thetvdb.com/banners/posters/78901-1.jpg
( Display in alphabetical order the state of all configuration variables )
> jamu -f
allgraphicsdir (~/Pictures)
bannerdir (None)
config_file (False)
data_flags (None)
debug_enabled (False)
download (False)
... lots of configuration variables ...
video_dir (None)
video_file_exts (['3gp', 'asf', 'asx', 'avi', 'mkv', 'mov', 'mp4', 'mpg', 'qt', 'rm', 'swf', 'wmv', 'm2ts', 'evo', 'ts', 'img', 'iso'])
with_ep_name (%(series)s - S%(seasonnumber)02dE%(episodenumber)02d - %(episodename)s.%(ext)s)
without_ep_name (%(series)s - S%(seasonnumber)02dE%(episodenumber)02d.%(ext)s)
'''
# System modules
import sys, os, re, locale, subprocess, locale, ConfigParser, urllib, codecs, shutil, datetime, fnmatch, string
from datetime import date
from optparse import OptionParser
from socket import gethostbyname
import tempfile, struct
import logging
class OutStreamEncoder(object):
"""Wraps a stream with an encoder"""
def __init__(self, outstream, encoding=None):
self.out = outstream
if not encoding:
self.encoding = sys.getfilesystemencoding()
else:
self.encoding = encoding
def write(self, obj):
"""Wraps the output stream, encoding Unicode strings with the specified encoding"""
if isinstance(obj, unicode):
try:
self.out.write(obj.encode(self.encoding))
except IOError:
pass
else:
try:
self.out.write(obj)
except IOError:
pass
def __getattr__(self, attr):
"""Delegate everything but write to the stream"""
return getattr(self.out, attr)
sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
if sys.version_info <= (2,5):
print '''JAMU requires Python 2.5 or newer to run.'''
sys.exit(1)
import xml.etree.cElementTree as ElementTree
# Find out if the MythTV python bindings can be accessed and instances can be created
try:
'''If the MythTV python interface is found, we can insert data directly to MythDB or
get the directories to store poster, fanart, banner and episode graphics.
'''
from MythTV import MythDB, Video, MythVideo, MythBE, MythError, MythLog, RecordedProgram
from MythTV.database import DBData
mythdb = None
mythvideo = None
mythbeconn = None
try:
'''Create an instance of each: MythDB, MythVideo
'''
MythLog._setlevel('none') # Some non option -M cannot have any logging on stdout
mythdb = MythDB()
mythvideo = MythVideo(mythdb)
MythLog._setlevel('important,general')
except MythError, e:
print u'\n! Warning - %s' % e.args[0]
filename = os.path.expanduser("~")+'/.mythtv/config.xml'
if not os.path.isfile(filename):
print u'\n! Warning - A correctly configured (%s) file must exist\n' % filename
else:
print u'\n! Warning - Check that (%s) is correctly configured\n' % filename
except Exception, e:
print u"\n! Warning - Creating an instance caused an error for one of: MythDB or MythVideo, error(%s)\n" % e
localhostname = mythdb.gethostname()
try:
MythLog._setlevel('none') # Some non option -M cannot have any logging on stdout
mythbeconn = MythBE(backend=localhostname, db=mythdb)
MythLog._setlevel('important,general')
except MythError, e:
print u'\nWith any -M option Jamu must be run on a MythTV backend'
print u'! Warning - %s' % e.args[0]
mythbeconn = None
except Exception, e:
print u"\n! Warning - MythTV python bindings could not be imported, error(%s)\n" % e
mythdb = None
mythvideo = None
mythbeconn = None
# Verify that tvdb_api.py, tvdb_ui.py and tvdb_exceptions.py are available
try:
# thetvdb.com specific modules
import MythTV.ttvdb.tvdb_ui as tvdb_ui
# from tvdb_api import Tvdb
import MythTV.ttvdb.tvdb_api as tvdb_api
from MythTV.ttvdb.tvdb_exceptions import (tvdb_error, tvdb_shownotfound, tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_episodenotfound, tvdb_attributenotfound, tvdb_userabort)
# verify version of tvdbapi to make sure it is at least 1.0
if tvdb_api.__version__ < '1.0':
print "\nYour current installed tvdb_api.py version is (%s)\n" % tvdb_api.__version__
raise
except Exception, e:
print '''
The modules tvdb_api.py (v1.0.0 or greater), tvdb_ui.py, tvdb_exceptions.py and cache.py.
They should have been installed along with the MythTV python bindings.
Error(%s)
''' % e
sys.exit(1)
try:
import MythTV.tmdb.tmdb_api as tmdb_api
from MythTV.tmdb.tmdb_exceptions import (TmdBaseError, TmdHttpError, TmdXmlError, TmdbUiAbort, TmdbMovieOrPersonNotFound,)
except Exception, e:
sys.stderr.write('''
The subdirectory "tmdb" containing the modules tmdb_api.py (v0.1.3 or greater), tmdb_ui.py,
tmdb_exceptions.py must have been installed with the MythTV python bindings.
Error:(%s)
''' % e)
sys.exit(1)
if tmdb_api.__version__ < '0.1.3':
sys.stderr.write("\n! Error: Your current installed tmdb_api.py version is (%s)\nYou must at least have version (0.1.3) or higher.\n" % tmdb_api.__version__)
sys.exit(1)
imdb_lib = True
try: # Check if the installation is equiped to directly search IMDB for movies
import imdb
except ImportError, e:
sys.stderr.write("\n! Error: To search for movies movies the IMDbPy library must be installed."\
"Check your installation's repository or check the following link."\
"from (http://imdbpy.sourceforge.net/?page=download)\nError:(%s)\n" % e)
sys.exit(1)
if imdb_lib:
if imdb.__version__ < "3.8":
sys.stderr.write("\n! Error: You version the IMDbPy library (%s) is too old. You must use version 3.8 of higher." % imdb.__version__)
sys.stderr.write("Check your installation's repository or check the following link."\
"from (http://imdbpy.sourceforge.net/?page=download)\n")
sys.exit(1)
class VideoTypes( DBData ):
_table = 'videotypes'
_where = 'intid=%s'
_setwheredat = 'self.intid,'
_logmodule = 'Python VideoType'
def __str__(self):
return "<VideoTypes '%s'>" % self.extension
def __repr__(self):
return str(self).encode('utf-8')
def __init__(self, id=None, ext=None, db=None):
if id is not None:
DBData.__init__(self, data=(id,), db=db)
elif ext is not None:
self.__dict__['_where'] = 'extension=%s'
self.__dict__['_wheredat'] = 'self.extension,'
DBData.__init__(self, data=(ext,), db=db)
else:
DBData.__init__(self, None, db=db)
# end VideoTypes()
def isValidPosixFilename(name, NAME_MAX=255):
"""Checks for a valid POSIX filename
Filename: a name consisting of 1 to {NAME_MAX} bytes used to name a file.
The characters composing the name may be selected from the set of
all character values excluding the slash character and the null byte.
The filenames dot and dot-dot have special meaning.
A filename is sometimes referred to as a "pathname component".
name: (base)name of the file
NAME_MAX: is defined in limits.h (implementation-defined constants)
Maximum number of bytes in a filename
(not including terminating null).
Minimum Acceptable Value: {_POSIX_NAME_MAX}
_POSIX_NAME_MAX: Maximum number of bytes in a filename
(not including terminating null).
Value: 14
More information on http://www.opengroup.org/onlinepubs/009695399/toc.htm
"""
return 1<=len(name)<= NAME_MAX and "/" not in name and "\000" not in name
# end isValidPosixFilename()
# Two routines used for movie title search and matching
def is_punct_char(char):
'''check if char is punctuation char
return True if char is punctuation
return False if char is not punctuation
'''
return char in string.punctuation
def is_not_punct_char(char):
'''check if char is not punctuation char
return True if char is not punctuation
return False if chaar is punctuation
'''
return not is_punct_char(char)
def _getExtention(URL):
"""Get the graphic file extension from a URL
return the file extention from the URL
"""
(dirName, fileName) = os.path.split(URL)
(fileBaseName, fileExtension)=os.path.splitext(fileName)
return fileExtension[1:]
# end getExtention
def _getFileList(dst):
''' Create an array of fully qualified file names
return an array of file names
'''
file_list = []
names = []
try:
for directory in dst:
try:
directory = unicode(directory, 'utf8')
except (UnicodeEncodeError, TypeError):
pass
for filename in os.listdir(directory):
names.append(os.path.join(directory, filename))
except OSError, e:
sys.stderr.write(u"\n! Error: Getting a list of files for directory (%s)\nThis is most likely a 'Permission denied' error\nError:(%s)\n\n" % (dst, e))
return file_list
for video_file in names:
if os.path.isdir(video_file):
new_files = _getFileList([video_file])
for new_file in new_files:
file_list.append(new_file)
else:
file_list.append(video_file)
return file_list
# end _getFileList
class singleinstance(object):
'''
singleinstance - based on Windows version by Dragan Jovelic this is a Linux
version that accomplishes the same task: make sure that
only a single instance of an application is running.
'''
def __init__(self, pidPath):
'''
pidPath - full path/filename where pid for running application is to be
stored. Often this is ./var/<pgmname>.pid
'''
from os import kill
self.pidPath=pidPath
#
# See if pidFile exists
#
if os.path.exists(pidPath):
#
# Make sure it is not a "stale" pidFile
#
try:
pid=int(open(pidPath, 'r').read().strip())
#
# Check list of running pids, if not running it is stale so
# overwrite
#
try:
kill(pid, 0)
pidRunning = 1
except OSError:
pidRunning = 0
if pidRunning:
self.lasterror=True
else:
self.lasterror=False
except:
self.lasterror=False
else:
self.lasterror=False
if not self.lasterror:
#
# Write my pid into pidFile to keep multiple copies of program from
# running.
#
fp=open(pidPath, 'w')
fp.write(str(os.getpid()))
fp.close()
def alreadyrunning(self):
return self.lasterror
def __del__(self):
if not self.lasterror:
import os
os.unlink(self.pidPath)
# end singleinstance()
# Global variables
graphicsDirectories = {'banner': u'bannerdir', 'screenshot': u'episodeimagedir', 'coverfile': u'posterdir', 'fanart': u'fanartdir'}
dir_dict={'posterdir': "VideoArtworkDir", 'bannerdir': 'mythvideo.bannerDir', 'fanartdir': 'mythvideo.fanartDir', 'episodeimagedir': 'mythvideo.screenshotDir', 'mythvideo': 'VideoStartupDir'}
storagegroupnames = {u'Videos': u'mythvideo', u'Coverart': u'posterdir', u'Banners': u'bannerdir', u'Fanart': u'fanartdir', u'Screenshots': u'episodeimagedir'}
storagegroups={u'mythvideo': [], u'posterdir': [], u'bannerdir': [], u'fanartdir': [], u'episodeimagedir': []} # The gobal dictionary is only populated with the current hosts storage group entries
image_extensions = ["png", "jpg", "bmp"]
def getStorageGroups():
'''Populate the storage group dictionary with the local host's storage groups.
return nothing
'''
records = mythdb.getStorageGroup(hostname=localhostname)
for record in records:
# Only include Video, coverfile, banner, fanart, screenshot and trailers storage groups
if record.groupname in storagegroupnames.keys():
dirname = record.dirname
try:
dirname = unicode(record.dirname, 'utf8')
except (UnicodeDecodeError):
sys.stderr.write(u"\n! Error: The local Storage group (%s) directory contained\ncharacters that caused a UnicodeDecodeError. This storage group has been rejected.'\n" % (record['groupname']))
continue # Skip any line that has non-utf8 characters in it
except (UnicodeEncodeError, TypeError):
pass
# Strip the trailing slash so it is consistent with all other directory paths in Jamu
if dirname[-1:] == u'/':
storagegroups[storagegroupnames[record.groupname]].append(dirname[:-1])
else:
storagegroups[storagegroupnames[record.groupname]].append(dirname)
continue
any_storage_group = False
tmp_storagegroups = dict(storagegroups)
for key in tmp_storagegroups.keys():
if len(tmp_storagegroups[key]):
any_storage_group = True
else:
del storagegroups[key] # Remove empty SG directory arrays
if any_storage_group:
# Verify that each storage group is an existing local directory
storagegroup_ok = True
for key in storagegroups.keys():
for directory in storagegroups[key]:
if not os.access(directory, os.F_OK):
sys.stderr.write(u"\n! Error: The local Storage group (%s) directory (%s) does not exist\n" % (key, directory))
storagegroup_ok = False
if not storagegroup_ok:
sys.exit(1)
# end getStorageGroups
def _can_int(x):
"""Takes a string, checks if it is numeric.
>>> _can_int("2")
True
>>> _can_int("A test")
False
"""
if x == None:
return False
try:
int(x)
except ValueError:
return False
else:
return True
# end _can_int
class BaseUI:
"""Default non-interactive UI, which auto-selects first results
"""
def __init__(self, config, log):
self.config = config
self.log = log
def selectSeries(self, allSeries):
return allSeries[0]
def selectMovieOrPerson(self, allElements):
return makeDict([allElements[0]])
# Local variable
video_type = u''
UI_title = u''
UI_search_language = u''
UI_selectedtitle = u''
# List of language from http://www.thetvdb.com/api/0629B785CE550C8D/languages.xml
# Hard-coded here as it is realtively static, and saves another HTTP request, as
# recommended on http://thetvdb.com/wiki/index.php/API:languages.xml
UI_langid_dict = {u'da': u'10', 'fi': u'11', u'nl': u'13', u'de': u'14', u'it': u'15', u'es': u'16', u'fr': u'17', u'pl': u'18', u'hu': u'19', u'el': u'20', u'tr': u'21', u'ru': u'22', u'he': u'24', u'ja': u'25', u'pt': u'26', u'zh': u'27', u'cs': u'28', u'sl': u'30', u'hr': u'31', u'ko': u'32', u'en': '7', u'sv': u'8', u'no': u'9',}
class jamu_ConsoleUI(BaseUI):
"""Interactively allows the user to select a show or movie from a console based UI
"""
def _displaySeries(self, allSeries_array):
"""Helper function, lists series with corresponding ID
"""
if video_type == u'IMDB':
URL = u'http://www.imdb.com/title/tt'
URL2 = u'http://www.imdb.com/find?s=all&q='+urllib.quote_plus(UI_title.encode("utf-8"))+'&x=0&y=0'
reftype = u'IMDB'
elif video_type == u'TMDB':
URL = u'http://themoviedb.org/movie/'
URL2 = u'http://themoviedb.org/'
reftype = u'TMDB'
else: # TVDB
URL = u'http://thetvdb.com/index.php?tab=series&id=%s&lid=%s'
URL2 = u'http://thetvdb.com/?tab=advancedsearch'
reftype = u'thetvdb'
tmp_title = u''
allSeries={}
for index in range(len(allSeries_array)):
allSeries[allSeries_array[index]['name']] = allSeries_array[index]
tmp_names = allSeries.keys()
tmp_names.sort()
most_likely = []
# Find any TV Shows or Movies who's titles start with the video's title
for name in tmp_names:
if filter(is_not_punct_char, name.lower()).startswith(filter(is_not_punct_char, UI_title.lower())):
most_likely.append(name)
# IMDB can return titles that are a movies foriegn title. The titles that do not match
# the requested title need to be added to the end of the most likely titles list.
if video_type == u'IMDB' and len(most_likely):
for name in tmp_names:
try:
dummy = most_likely.index(name)
except ValueError:
most_likely.append(name)
names = []
# Remove any name that does not start with a title like the TV Show/Movie (except for IMDB)
if len(most_likely):
for likely in most_likely:
names.append(likely)
else:
names = tmp_names
if not video_type == u'IMDB':
names.sort()
# reorder the list of series and sid's
new_array=[]
for key in names: # list all search results
new_array.append(allSeries[key])
# If there is only one to select and it is an exact match then return with no interface display
if len(new_array) == 1:
if filter(is_not_punct_char, allSeries_array[0]['name'].lower()) == filter(is_not_punct_char, UI_title.lower()):
return new_array
# Add the ability to select the skip inetref of '99999999'
new_array.append( {'sid': '99999999', 'name': u'User choses to ignore video'} )
names.append(u'User choses to ignore video')
i_show=0
for key in names: # list all search results
i_show+=1 # Start at more human readable number 1 (not 0)
if key == u'User choses to ignore video':
print u"% 2s -> %s # %s" % (
i_show,
'99999999', "Set this video to be ignored by Jamu with a reference number of '99999999'"
)
continue
if video_type != u'IMDB' and video_type != u'TMDB':
tmp_URL = URL % (allSeries[key]['sid'], UI_langid_dict[UI_search_language])
print u"% 2s -> %s # %s" % (
i_show,
key, tmp_URL
)
else:
print u"% 2s -> %s # %s%s" % (
i_show,
key, URL,
allSeries[key]['sid']
)
print u"Direct search of %s # %s" % (
reftype,
URL2
)
return new_array
def selectSeries(self, allSeries):
global UI_selectedtitle
UI_selectedtitle = u''
allSeries = self._displaySeries(allSeries)
# Check for an automatic choice
if len(allSeries) <= 2:
for series in allSeries:
if filter(is_not_punct_char, series['name'].lower()) == filter(is_not_punct_char, UI_title.lower()):
UI_selectedtitle = series['name']
return series
display_total = len(allSeries)
if video_type == u'IMDB':
reftype = u'IMDB #'
refsize = 7
refformat = u"%07d"
elif video_type == u'TMDB':
reftype = u'TMDB #'
refsize = 5
refformat = u"%05d"
else:
reftype = u'Series id'
refsize = 5
refformat = u"%6d" # Attempt to have the most likely TV/Movies at the top of the list
while True: # return breaks this loop
try:
print u'Enter choice ("Enter" key equals first selection (1)) or input the %s directly, ? for help):' % reftype
ans = raw_input()
except KeyboardInterrupt:
raise tvdb_userabort(u"User aborted (^c keyboard interupt)")
except EOFError:
raise tvdb_userabort(u"User aborted (EOF received)")
self.log.debug(u'Got choice of: %s' % (ans))
try:
if ans == '':
selected_id = 0
else:
selected_id = int(ans) - 1 # The human entered 1 as first result, not zero
except ValueError: # Input was not number
if ans == u"q":
self.log.debug(u'Got quit command (q)')
raise tvdb_userabort(u"User aborted ('q' quit command)")
elif ans == u"?":
print u"## Help"
print u"# Enter the number that corresponds to the correct video."
print u"# Enter the %s number for the %s." % (reftype, video_type)
print u"# ? - this help"
print u"# q - abort"
else:
self.log.debug(u'Unknown keypress %s' % (ans))
else:
self.log.debug(u'Trying to return ID: %d' % (selected_id))
try:
UI_selectedtitle = allSeries[selected_id]['name']
return allSeries[selected_id]
except IndexError:
if len(ans) == refsize and reftype != u'Series id':
UI_selectedtitle = u''
return {'name': u'User input', 'sid': ans}
elif reftype == u'Series id':
if len(ans) >= refsize:
UI_selectedtitle = u''
return {'name': u'User input', 'sid': ans}
self.log.debug(u'Invalid number entered!')
print u'Invalid number (%d) input! A directly entered %s must be a full %d zero padded digits (e.g. 905 should be entered as %s)' % (selected_id, reftype, refsize, refformat % 905)
UI_selectedtitle = u''
self._displaySeries(allSeries)
#end try
#end while not valid_input