├── .gitignore ├── BUGS.txt ├── Changelog.md ├── Development.md ├── LICENSE.txt ├── PROGRAMMING NOTES.txt ├── RESOURCES ├── Readme.md ├── ToDo.md ├── design.pdf ├── examples ├── Collateral.2004.720p.BrRip.x264.YIFY.mp4 ├── Jaws.1975.720p.BrRip.x264.bitloks.YIFY.mp4 └── L'Esorcista [Divx -ITA] [Anacletus].avi ├── new icons ├── ic_add_black_48px.svg ├── ic_check_black_48px.svg ├── ic_clear_black_48px.svg ├── ic_create_black_48px.svg ├── ic_folder_black_48px.svg ├── ic_info_black_48px.svg ├── ic_label_black_48px.svg ├── ic_local_movies_black_48px.svg ├── ic_remove_black_48px.svg ├── ic_settings_black_48px.svg └── ic_warning_black_48px.svg ├── pack_source.py ├── publish_executable.py ├── publish_source.py ├── setup.py ├── src ├── __init__.py ├── application.py ├── exceptionhandler.py ├── guess.py ├── icons │ ├── brand.ico │ ├── brand.png │ ├── cross.png │ ├── eraser.png │ ├── exclamation.png │ ├── film.png │ ├── information.png │ ├── magnifier.png │ ├── minus.png │ ├── movie_add.png │ ├── movie_erase.png │ ├── movie_remove.png │ ├── movies_from_folder.png │ ├── pencil.png │ ├── plus.png │ ├── tag.png │ ├── tick.png │ └── wrench-screwdriver.png ├── languages.txt ├── main_window.py ├── movie.py ├── movie_file_info.py ├── movie_guessed_info.py ├── movie_info.py ├── movie_tmdb_info.py ├── preferences.py ├── preferences_dialog.py ├── renaming_rule_dialog.py ├── stats_agreement_dialog.py ├── ui │ ├── ALBusyIndicator.qml │ ├── main_window.qml │ ├── main_window.ui │ ├── main_window_controller.py │ ├── main_window_view.py │ ├── movie_table_item.py │ ├── preferences_dialog.ui │ ├── renaming_rule_dialog.ui │ ├── renaming_rule_window.qml │ ├── renaming_rule_window_controller.py │ ├── renaming_rule_window_view.py │ ├── rules_list_item.py │ └── stats_agreement_dialog.ui └── utils.py ├── stats.py └── upload2google.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | # PyCharm 63 | .idea -------------------------------------------------------------------------------- /BUGS.txt: -------------------------------------------------------------------------------- 1 | 2 | ---------------Baraka_1992_DVDrip_Xvid-Ekolb.avi----------- 3 | Traceback (most recent call last): 4 | File "D:\Alberto\workspace\almoviesrenamer\src\movie.py", line 215, in __init__ 5 | video_info = enzyme.parse(self.abs_original_file_name()) 6 | File "D:\Alberto\workspace\almoviesrenamer\src\enzyme\__init__.py", line 62, in parse 7 | p = mod.Parser(f) 8 | File "D:\Alberto\workspace\almoviesrenamer\src\enzyme\mkv.py", line 401, in __init__ 9 | self.process_elem(elem) 10 | File "D:\Alberto\workspace\almoviesrenamer\src\enzyme\mkv.py", line 441, in process_elem 11 | self.process_seekhead(elem) 12 | File "D:\Alberto\workspace\almoviesrenamer\src\enzyme\mkv.py", line 473, in process_seekhead 13 | self.process_elem(elem) 14 | File "D:\Alberto\workspace\almoviesrenamer\src\enzyme\mkv.py", line 432, in process_elem 15 | self.process_tracks(elem) 16 | File "D:\Alberto\workspace\almoviesrenamer\src\enzyme\mkv.py", line 482, in process_tracks 17 | self.process_track(trackelem) 18 | File "D:\Alberto\workspace\almoviesrenamer\src\enzyme\mkv.py", line 513, in process_track 19 | track = self.process_video_track(elements) 20 | File "D:\Alberto\workspace\almoviesrenamer\src\enzyme\mkv.py", line 583, in process_video_track 21 | track.aspect = float(d_width) / d_height 22 | ZeroDivisionError: float division by zero 23 | Exception in thread Thread-2: 24 | Traceback (most recent call last): 25 | File "C:\Python27\lib\threading.py", line 551, in __bootstrap_inner 26 | self.run() 27 | File "C:\Python27\lib\threading.py", line 504, in run 28 | self.__target(*self.__args, **self.__kwargs) 29 | File "D:\Alberto\workspace\almoviesrenamer\src\gui.py", line 295, in load_movies_run 30 | movie.generate_new_name(renaming_rule) 31 | File "D:\Alberto\workspace\almoviesrenamer\src\movie.py", line 636, in generate_new_name 32 | and self.language() != '': 33 | File "D:\Alberto\workspace\almoviesrenamer\src\movie.py", line 331, in language 34 | return self.info_[self.LANGUAGE][index] 35 | TypeError: 'NoneType' object has no attribute '__getitem__' 36 | 37 | ---------------HERO, searching Ying Xiong ----------- 38 | Traceback (most recent call last): 39 | File "D:\Alberto\workspace\almoviesrenamer\src\gui.py", line 520, in movies_selection_changed 40 | self.populate_movie_panel() 41 | File "D:\Alberto\workspace\almoviesrenamer\src\gui.py", line 578, in populate_movie_panel 42 | for other_info in movie.others_info(): 43 | File "D:\Alberto\workspace\almoviesrenamer\src\movie.py", line 398, in others_info 44 | others_info.append(info) 45 | TypeError: 'NoneType' object has no attribute '__getitem__' 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | ### 6 2 | 3 | - Updated frameworks: Python 3.5, PyQt 5.5 (based on Qt 5.5) 4 | - Dropped translations (sorry for that) 5 | 6 | ### 5 7 | 8 | - CHG: small changes in title cleanup 9 | - CHG: better organization of movie information panel 10 | - FIX: corrections with some titles detection 11 | - ADD: the application now warns you with a sound, when information search finishes 12 | - ADD: "advertising" label during information search 13 | - ADD: files with same names are now properly handled, appending a counter at file name's end 14 | - ADD: subtitles detection 15 | 16 | ### 4 17 | 18 | - CHG: new algorithm for generating new file name 19 | - ADD: double clicking on a movie opens file manager in containing folder 20 | - ADD: right click on a movie to open containing folder or copy file name 21 | - CHG: now www.imdbapi.com is used to retrieve correct movie from guessed title. this speeds up movies info retrieving time, and information correctness 22 | - CHG: better organization of attributes on renaming rule dialog 23 | - FIX: language detection corrections under Linux 24 | 25 | ### 3.1 26 | 27 | - CHG: some corrections in guessing info from file name 28 | - ADD: executable icon 29 | - FIX: statistics where not correctly handled 30 | 31 | ### 3.0 32 | 33 | - CHG: improvements on movies rename process, with a new way for selecting movies information 34 | - CHG: better movies recognition 35 | - CHG: new movies attributes, and better representation 36 | - ADD: the representation of some movie attributes and words separator can now be personalized 37 | - ADD: enzyme module to get video duration 38 | - CHG: a lot of other small changes and improvements 39 | 40 | ### 2.1 41 | 42 | - CHG: improvements on movies loading and on manual title searching 43 | - ADD: internet connection check during program loading 44 | - ADD: usage statistics (agreement + settings) 45 | - CHG: more elements at once, in renaming rules, can now be selected and moved or removed 46 | - ADD: check for new program version 47 | - FIX: now also movie part is translated 48 | 49 | ### 2.0 50 | 51 | - CHG: almost completely new GUI, with a totally new and faster renaming work flow. now there is only one renaming rule, used for all the movies, which can be created in a new, easier way. this rule is also stored on file. 52 | - CHG: the program is smarter on recognizing movie titles, and it tries to reduce user intervention on defining the names 53 | - CHG: dropped most of the movie attributes 54 | - CHG: dropped use of Hachoir parser 55 | - ADD: italian translation 56 | 57 | ### 1.2.0 58 | 59 | - ADD: a new way to notify finished renaming process: movies will be highlighted in green, red or white based on renaming succes (renaming done without problems, errors on renaming, nothing to rename). a summary panel will also be displayed showing information on errors. 60 | - CHG: "Rename" button renamed into a better-explaining "Rename checked movies" 61 | - CHG: unchecked items are now updated when its renaming pattern changes, and are colored in gray, showing they will not be renamed 62 | - CHG: better info guessing from file name and better title cleaning (utils.guessInfoFromFileName and utils.cleanTitle) 63 | 64 | ### 1.1.0 65 | 66 | - ADD: a button for cleaning the renaming rule 67 | - ADD: IMDbPY and Hachoir versions to the log information 68 | - CHG: now each movie has his own rename rule 69 | - CHG: generate new movie title only when movie is checked 70 | - CHG: no more using a new thread to generate new movie title 71 | - CHG: the "rename" button has been moved at the bottom of the panel 72 | - CHG: when searching for a title manually, now the search field remembers the text 73 | - FIX: an error occurred when the "runtime" metadata was empty 74 | - FIX: with some files Hachoir returns a duration containing also a time in milliseconds. now this time is removed 75 | - FIX: no more exceptions are raised when the generateNewTitle function is called 76 | - FIX: akas information on windows and linux have different syntax, now is taken into account when getting the akas 77 | 78 | ### 1.0.0 79 | 80 | - ADD: run times guessing 81 | - ADD: three new buttons in "rename" tab, useful for checking or unchecking files to rename 82 | - CHG: the "about" dialog message now uses the "Program" and "Version" info from utils 83 | - FIX: changed the "duration" metadata separator from ':' to 'h-m-s', because the ':' character is not allowed under Windows 84 | - FIX: now each movie info dictionary has his own "current index" for aka and runtime 85 | - FIX: removing of selected movies now works properly 86 | 87 | ### 0.4.0 88 | 89 | - ADD: the user can now decide whether to rename the file or not, using the appropriate checkbox near the new file name 90 | - ADD: "disk" property. the disk property is guessed from the file name. 91 | - ADD: guessing of movie language. the properly "aka" and "language" properties are selected based on this information. the language property is guessed from the file name and from system language. 92 | - ADD: a new panel is shown when title search in IMDB returns no results. this panel shows a text field where user can search for movie title manually. 93 | - CHG: file title cleaning before searching into IMDB 94 | - CHG: exception handler 95 | - CHG: cleaned video and movie information 96 | - CHG: using "unicode" now instead of "str" for each string 97 | - CHG: better organization in Movie class code 98 | - FIX: when changing a movie property (title, aka, ...) after having defined a renaming rule, the new name now it refreshes properly. 99 | - FIX: now the "original name" column in tables do not shows no more the file extension 100 | 101 | ### 0.3.0 102 | 103 | - ADD: language information can be added to new title 104 | - CHG: completed GUI 105 | 106 | ### 0.2.0 107 | 108 | - ADD: back button in manual title search 109 | - CHG: movie title search no more uses keywords. instead file name is cleaned -and used in search. 110 | - CHG: completed movies information request and visualization 111 | 112 | ### 0.1.0 113 | 114 | - first usable version, with basic features. 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /Development.md: -------------------------------------------------------------------------------- 1 | Reference guide to set up development environment. 2 | 3 | # Requirements 4 | 5 | - [Python 3.5](https://www.python.org/downloads/) 6 | - [Qt 5.5](http://www.qt.io/) (required only if compiling from source) 7 | - [SIP 4.17](https://www.riverbankcomputing.com/software/sip/download) (from source) 8 | - [PyQt5 5.5](https://www.riverbankcomputing.com/software/pyqt/download5) (from source) 9 | - [TMDBsimple 1.x](https://github.com/celiao/tmdbsimple/) 10 | - [guessit 1.x](https://github.com/wackou/guessit) 11 | - [enzyme](https://github.com/Diaoul/enzyme) 12 | 13 | # Mac OS X 14 | 15 | ## Install modules and tools 16 | 17 | - install [Brew](http://brew.sh/): 18 | 19 | `ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"` 20 | 21 | - install PyQt5 (this will also install Python 3.5 and SIP, if necessary): 22 | 23 | `brew install pyqt5` 24 | 25 | - install TMDBsimple 26 | 27 | `pip3 install tmdbsimple` 28 | 29 | - install guessit 30 | 31 | `pip3 install guessit` 32 | 33 | - install enzyme 34 | 35 | `pip3 install enzyme` 36 | 37 | ## Setup PyCharm 38 | 39 | - Settings > Project Interpreter > Add local... 40 | - Select Python interpreter from: 41 | 42 | `/usr/local/Cellar/python3/3.5.0/Frameworks/Python.framework/Versions/3.5/bin/python3.5` 43 | 44 | 45 | -------------------------------------------------------------------------------- /PROGRAMMING NOTES.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- 3 | nuovo numero versione 4 | commentare 5 | codificare/gui/icone/labels 6 | tooltips 7 | aggiornare "about" (anche librerie) 8 | traduzionare 9 | buildare translations 10 | changeloggare 11 | readme 12 | testare 13 | aggiornare setup.py 14 | buildare (win + linux) 15 | aggiornare versione su web service 16 | -------------------------------------------------------------------------------- 17 | 18 | 19 | 20 | IDE: Eclipse 3.7 21 | 22 | Docs: 23 | cx_Freeze - http://cx_freeze.readthedocs.org/en/latest/index.html 24 | 25 | 26 | 27 | lxml 28 | libxml2-dev 29 | libxslt-dev 30 | 31 | IMDbPY 32 | install lxml first 33 | [under Windows] python setup.py --without-cutils install 34 | 35 | Packaging: 36 | cx_Freeze 4.2 37 | python-dev [under Linux] 38 | libssl-dev [under Linux] 39 | 40 | 41 | 42 | The policy we use with PyQt is as follows: 43 | • Use type str only when working with strictly 7-bit ASCII strings or with 44 | raw 8-bit data, that is, with raw bytes. 45 | • For strings that will be used only by PyQt functions, for example, strings 46 | that are returned by one PyQt function only to be passed at some point to 47 | another PyQt function—do not convert such strings. Simply keep them as 48 | QStrings. 49 | • In all other cases, use unicode strings, converting QStrings to unicode as 50 | soon as possible. In other words, as soon as a QString has been returned 51 | from a Qt function, always immediately convert it to type unicode. 52 | This policy means that we avoid making incorrect assumptions about 8-bit 53 | string encodings (because we use Unicode). It also ensures that the strings we 54 | pass to Python functions have the methods that Python expects: QStrings have 55 | different methods from str and unicode, so passing them to Python functions 56 | can lead to errors. PyQt uses QString rather than unicode because when PyQt 57 | was first created, Python’s Unicode support was nowhere near as good as it 58 | is today. 59 | 60 | le liste sono mutabili, quindi se ne faccio delle copie e cambio le copie, 61 | anche l'originale cambia. per caso modifico copie di liste da qualche parte? 62 | 63 | usare i set quando voglio liste con item non duplicati 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /RESOURCES: -------------------------------------------------------------------------------- 1 | 2 | http://stackoverflow.com/tags/cx-freeze/hot 3 | 4 | http://stackoverflow.com/questions/2553886/how-can-i-bundle-other-files-when-using-cx-freeze/2892707#2892707 5 | 6 | http://pypi.python.org/pypi/esky/ 7 | 8 | https://www.google.com/search?q=cx-freeze+include+pyd+files+into+executable&hl=en&client=ubuntu&hs=icy&channel=fs&prmd=imvns&ei=XzdnT62ZLMf_4QTQ7o3fBw&start=10&sa=N&biw=1280&bih=686&cad=b&cad=cbv&sei=4jFsT8HpOYiAOvDr-fsF&qscrl=1 9 | 10 | http://gatc.ca/2009/12/23/python-program-executables/ 11 | 12 | almoviesrenamer.appspot.com/rulestats?getrules=yes 13 | 14 | https://appengine.google.com/dashboard?&app_id=s~almoviesrenamer 15 | 16 | https://developers.google.com/appengine/docs/java/ 17 | 18 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | ALMoviesRenamer cares about automatically rename your movie files, searching for information on the web. 4 | 5 | You will have video file names like this: 6 | 7 | _A.Really.Cool.Movie.2008.ENG.XviD-Republic.CD1_ 8 | 9 | renamed into this: 10 | 11 | _A really cool movie (2008, A. Director, 90') CD1_ 12 | 13 | Of course, you decide the rename pattern. 14 | 15 | # How to use 16 | 17 | Once selected the movies you want to rename, the program will automatically search the web for information like movie titles, year, directors. Than you have to choose the renaming rules, and that's it! 18 | 19 | # Development 20 | 21 | ALMoviesRenamer is programmed in Python, using [PyQt](http://www.riverbankcomputing.co.uk/software/pyqt/intro) 22 | as GUI support, and [IMDbPY](http://imdbpy.sourceforge.net/) to get movies information. 23 | 24 | ## Libraries 25 | 26 | - Python 2.7 27 | - PyQt 4.9 - http://www.riverbankcomputing.co.uk/software/pyqt/download 28 | - lxml 2.3 - http://pypi.python.org/pypi/lxml/2.3.3 29 | - IMDbPY 4.8 - http://imdbpy.sourceforge.net/ 30 | - enzyme 0.2 - https://github.com/Diaoul/enzyme 31 | - cx_Freeze 4.2 - http://cx-freeze.sourceforge.net/ 32 | 33 | ## License 34 | 35 | GNU General Public License version 3 (GPLv3) 36 | -------------------------------------------------------------------------------- /ToDo.md: -------------------------------------------------------------------------------- 1 | 2 | # guessit 3 | 4 | > If you have the 'guess-language' python package installed, GuessIt can also analyze a subtitle file's contents and detect which language it is written in. 5 | 6 | -[ ] investigate use of guess-language 7 | 8 | > If you have the 'enzyme' python package installed, GuessIt can also detect the properties from the actual video file metadata. 9 | 10 | -[ ] investigate use of enzyme 11 | 12 | # General 13 | 14 | -[ ] update guessit to version 2.x (when is released) 15 | -[ ] remove unused modules/files 16 | -[ ] add support for [Rotten Tomatoes API](https://pypi.python.org/pypi/rtsimple) 17 | -[ ] automatically download subtitles using [Subliminal](https://github.com/Diaoul/subliminal) 18 | -[ ] automatically rename subtitles if found in the same folder as a movie 19 | -[ ] replace QtWidgets with QML 20 | 21 | # To review 22 | 23 | in manual search, add ability to specify the language 24 | 25 | use video duration to get the best runtime 26 | 27 | sort movie info by title, year and language (need to implement also alternative_info sorting on movie) 28 | 29 | probabilmente la lingua guessed va raffinata usando quella di sistema, 30 | o comunque impostare quella di sistema come quella di default se non trovata nel titolo 31 | del file 32 | 33 | invece di sostituire i caratteri illegali dei sistemi operativi con underscore, 34 | usare caratteri piu utili, ad es. & -> and 35 | 36 | quando vengono identificate più lingue, bisogna decidere quale tenere (e forse dare la possibilità 37 | all'utente di scegliere successivamente). si potrebbero poi attribuire dei punteggi, ad esempio 38 | in base al contesto (se dentro parentesi puntoeggio più alto) e in base alle parole che ci 39 | sono prima/dopo (es: sub ITA) 40 | 41 | Preferences > Renaming rule > Words separator, change it into "attributes separator" 42 | 43 | l'indicazione della lingua col suo nome inglese completo 44 | (es. french) 45 | non viene riconosciuta... 46 | aggiungere questa feature? 47 | 48 | la ricerca su imdb molto spesso non ritorna la lingua del titolo, 49 | e se non recuperata dal titolo originale, rimane vuota. 50 | trovare una soluzione per avere sempre la lingua disposizione 51 | 52 | non sempre viene recuperato l'aka per tutte le lingue 53 | 54 | aggiungere la lingua di sistema come lingua selezionabile per il film 55 | 56 | usare la lingua di sistema come metodo di selezione del best aka, 57 | quando non presenti indicazioni della lingua nel titolo? 58 | 59 | aggiungere possibilità di avere più film con lo stesso nome, appendendo al nome del file un numero 60 | 61 | 62 | 63 | chiedere mailinglist cx-freeze come si includono tutte le dll e i pyd in library.zip 64 | (prima cercare in rete e nelle vecchie conversazioni della mailinglist) 65 | 66 | nuova identificazione di info dal titolo 67 | 68 | Esistono dei vocabolari liberi con la lista di parole usate un una certa lingua? 69 | Si potrebbero usare per fare il matching con le parole che compongono il titolo. 70 | Da lì si potrebbe estrarre sia il titolo, sia altre info 71 | (es. Quelle che non appartengono al titolo perché parole non presenti nel dizionario) 72 | e la lingua. Oppure cercare delle librerie che facciano già questa cosa 73 | (analisi dei testi, elaborazione) 74 | 75 | http://pypi.python.org/pypi/MontyLingua/2.1 76 | 77 | http://www.nltk.org/ 78 | 79 | implementare un metodo per recuperare dal nome del file le blackword e salvarle per future rinomine 80 | 81 | la lista delle blackword verrà salvata anche su web service e aggiornata ogni volta che si apre il programma. 82 | tutti gli uteni parteciperanno così alla creazione della blackwords list 83 | 84 | il file di setup dovrebbe impostare le settings in automatico in base alle statistiche di utilizzo 85 | 86 | rivedere i todo lasciati sparsi nel codice 87 | 88 | pare che al titolo originale venga associata al lingua guessed, invece della lingua del titolo originale.. controllare 89 | 90 | aggiungere la possibilità di specificare la lingua, magari solo la lingua di sistema 91 | 92 | non sempre la lingua del film è specificata nel titolo, e quindi non viene identificata. 93 | inoltre non sempre la lingua desiderata compare tra gli aka, quindi c'è la necessità 94 | di specificarla manualmente. si potrebbe quindi pensare ad una combobox per la lingua 95 | in cui si può scegliere la lingua di sistema come lingua alternativa 96 | 97 | chiedere all'autore di http://code.google.com/p/the-better-renamer/ se vuole unirsi al progetto 98 | 99 | chiedere a quelli di lifehacker se mi fanno pubblicità 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /design.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/design.pdf -------------------------------------------------------------------------------- /examples/Collateral.2004.720p.BrRip.x264.YIFY.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/examples/Collateral.2004.720p.BrRip.x264.YIFY.mp4 -------------------------------------------------------------------------------- /examples/Jaws.1975.720p.BrRip.x264.bitloks.YIFY.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/examples/Jaws.1975.720p.BrRip.x264.bitloks.YIFY.mp4 -------------------------------------------------------------------------------- /examples/L'Esorcista [Divx -ITA] [Anacletus].avi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/examples/L'Esorcista [Divx -ITA] [Anacletus].avi -------------------------------------------------------------------------------- /new icons/ic_add_black_48px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /new icons/ic_check_black_48px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /new icons/ic_clear_black_48px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /new icons/ic_create_black_48px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /new icons/ic_folder_black_48px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /new icons/ic_info_black_48px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /new icons/ic_label_black_48px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /new icons/ic_local_movies_black_48px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /new icons/ic_remove_black_48px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /new icons/ic_settings_black_48px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /new icons/ic_warning_black_48px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pack_source.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | import application 3 | 4 | __author__ = "Alberto Malagoli" 5 | 6 | from PyQt4.QtCore import QSettings 7 | from glob import glob 8 | import os.path 9 | import os 10 | import shutil 11 | import sys 12 | sys.path.append('src') 13 | import utils 14 | 15 | ## change setting first_time before building 16 | # load settings 17 | settings = QSettings("src/preferences.ini", QSettings.IniFormat) 18 | # save value on settings file 19 | settings.setValue("first_time", True) 20 | settings.setValue("stats_agreement", 1) 21 | settings.setValue("duration_representation", 0) 22 | settings.setValue("language_representation", 1) 23 | settings.setValue("words_separator", 0) 24 | settings.setValue("last_visited_directory", "") 25 | settings.setValue("renaming_rule", "title.(.year.duration.language.)") 26 | settings.sync() 27 | 28 | if os.path.isdir('src/log'): 29 | shutil.rmtree('src/log') 30 | 31 | if os.path.isdir('tmp'): 32 | print("remove previously created tmp folder") 33 | shutil.rmtree('tmp') 34 | print("create tmp folder") 35 | os.mkdir("tmp") 36 | print("copying source files on it...") 37 | shutil.copytree("src", "tmp/src") 38 | for f in glob("*.py"): 39 | shutil.copy2(f, "tmp") 40 | for f in glob("*.txt"): 41 | shutil.copy2(f, "tmp") 42 | print("files copied") 43 | 44 | archive_name = "dist/{0}-{1}-src" \ 45 | .format(application.NAME, application.VERSION) 46 | archive_file_name = "{0}.tar.gz".format(archive_name) 47 | if os.path.isfile(archive_file_name): 48 | print("remove previously created archive") 49 | os.remove(archive_file_name) 50 | root_dir = "tmp" #os.path.abspath("tmp") 51 | print("create gztar " + archive_name) 52 | shutil.make_archive(archive_name, "gztar", root_dir = root_dir) 53 | print("remove tmp folder") 54 | 55 | print("END") 56 | 57 | 58 | -------------------------------------------------------------------------------- /publish_executable.py: -------------------------------------------------------------------------------- 1 | import application 2 | from upload2google import upload2google 3 | import platform 4 | import sys 5 | sys.path.append('src') 6 | sys.path.append('.') 7 | import utils 8 | 9 | if platform.system() == "Windows": 10 | extension = "zip" 11 | else: 12 | extension = "tar.gz" 13 | file_path = "dist/{0}-{1}-{2}.{3}" \ 14 | .format(application.NAME, application.VERSION, platform.system(), extension) 15 | project = "almoviesrenamer" 16 | summary = application.NAME + " " + application.VERSION + " " + platform.system() 17 | labels = ["Featured", "Type-Archive"] 18 | if platform.system() == "Windows": 19 | labels.append("OpSys-Windows") 20 | else: 21 | labels.append("OpSys-Linux") 22 | 23 | upload2google(file_path, project, summary, labels) 24 | 25 | -------------------------------------------------------------------------------- /publish_source.py: -------------------------------------------------------------------------------- 1 | import application 2 | from upload2google import upload2google 3 | import platform 4 | import sys 5 | sys.path.append('src') 6 | sys.path.append('.') 7 | import utils 8 | 9 | file_path = "dist/{0}-{1}-src.{2}" \ 10 | .format(application.NAME, application.VERSION, "tar.gz") 11 | project = "almoviesrenamer" 12 | summary = application.NAME + " " + application.VERSION + " Source" 13 | labels = ["Featured", "Type-Source", "OpSys-All"] 14 | 15 | upload2google(file_path, project, summary, labels) 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | import application 3 | 4 | __author__ = "Alberto Malagoli" 5 | 6 | from PyQt4.QtCore import QSettings 7 | from cx_Freeze import setup, Executable 8 | from glob import glob 9 | import os.path 10 | import os 11 | import shutil 12 | import sys 13 | import platform 14 | sys.path.append('src') 15 | import utils 16 | 17 | ## change setting first_time before building 18 | # load settings 19 | settings = QSettings("src/preferences.ini", QSettings.IniFormat) 20 | # save value on settings file 21 | settings.setValue("first_time", True) 22 | settings.setValue("stats_agreement", 1) 23 | settings.setValue("duration_representation", 0) 24 | settings.setValue("language_representation", 1) 25 | settings.setValue("words_separator", 0) 26 | settings.setValue("last_visited_directory", "") 27 | settings.setValue("renaming_rule", "title.(.year.duration.language.)") 28 | settings.sync() 29 | 30 | if os.path.isdir('build'): 31 | print("removing build path...") 32 | shutil.rmtree('build') 33 | if os.path.isdir('src/log'): 34 | print("removing log path...") 35 | shutil.rmtree('src/log') 36 | 37 | includes = [ 38 | 'enzyme', 39 | ] 40 | 41 | excludes = [ 42 | 'email', 43 | 'unittest', 44 | '_hashlib', 45 | '_ssl', 46 | '_sqlite3', 47 | 'sqlite3', 48 | 'sqlobject', 49 | 'bz2', 50 | 'select', 51 | '_codecs_cn', 52 | '_codecs_hk', 53 | '_codecs_iso2022', 54 | '_codecs_jp', 55 | '_codecs_kr', 56 | '_codecs_tw', 57 | '_ctypes', 58 | '_heapq', 59 | 'sqlalchemy.cprocessors', 60 | 'sqlalchemy.cresultproxy', 61 | 'PyQt4._qt', 62 | '_json', 63 | '_multibytecodec', 64 | 'termios' 65 | ] 66 | 67 | include_files = [ 68 | ("src/enzyme", "enzyme"), 69 | ("src/icons", "icons"), 70 | ("src/translations", "qm"), 71 | ("src/main_window.ui", "main_window.ui"), 72 | ("src/preferences_dialog.ui", "preferences_dialog.ui"), 73 | ("src/renaming_rule_dialog.ui", "renaming_rule_dialog.ui"), 74 | ("src/stats_agreement_dialog.ui", "stats_agreement_dialog.ui"), 75 | ("src/preferences.ini", "preferences.ini"), 76 | ("src/languages.txt", "languages.txt"), 77 | "Changelog.md", 78 | "gpl-3.0.txt", 79 | "README.txt", 80 | ] 81 | 82 | exclude_files = [ 83 | "QtNetwork4.dll", 84 | "QtWebKit4.dll", 85 | "libphonon.so.4", 86 | "libQtDBus.so.4", 87 | "libQtDeclarative.so.4", 88 | "libQtMultimedia.so.4", 89 | "libQtScript.so.4", 90 | "libQtScriptTools.so.4", 91 | "libQtSql.so.4", 92 | "libQtSvg.so.4", 93 | "libQtTest.so.4", 94 | "libQtWebKit.so.4", 95 | "libQtNetwork.so.4", 96 | "libQtXml.so.4", 97 | "libQtXml.so.4", 98 | "libQtXmlPatterns.so.4", 99 | ] 100 | 101 | base = None 102 | target_name = application.NAME 103 | archive_format = "gztar" 104 | if sys.platform == "win32": 105 | base = "Win32GUI" 106 | target_name += ".exe" 107 | archive_format = "zip" 108 | 109 | setup( 110 | name =application.NAME, 111 | version =application.VERSION, 112 | author = "Alberto Malagoli", 113 | author_email = 'albemala@gmail.com', 114 | url = 'https://code.google.com/p/almoviesrenamer/', 115 | options = dict( 116 | build_exe = {"includes": includes, 117 | "excludes": excludes, 118 | "include_files": include_files, 119 | "bin_excludes": exclude_files, 120 | "optimize": 2, 121 | "compressed": True, # Compress library.zip 122 | "create_shared_zip": True 123 | }), 124 | executables = [Executable( 125 | script = "src/main.py", 126 | base = base, 127 | targetName = target_name, 128 | icon = "src/icons/brand.ico", 129 | compress = True, 130 | )] 131 | ) 132 | 133 | shutil.rmtree(glob("build/exe*")[0] + '/PyQt4.uic.widget-plugins') 134 | 135 | archive_name = "dist/{0}-{1}-{2}" \ 136 | .format(application.NAME, application.VERSION, platform.system()) 137 | archive_file_name = glob("{0}*".format(archive_name)) 138 | if len(archive_file_name) > 0 \ 139 | and os.path.isfile(archive_file_name[0]): 140 | print("remove previously created archive") 141 | os.remove(archive_file_name[0]) 142 | root_dir = glob("build/exe*")[0] 143 | print("create " + archive_format + " " + archive_name) 144 | shutil.make_archive(archive_name, archive_format, root_dir) 145 | 146 | print("END") 147 | 148 | 149 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Alberto Malagoli" 5 | 6 | try: 7 | import sys 8 | from PyQt5.QtWidgets import QApplication 9 | import utils 10 | from ui.main_window_controller import MainWindowController 11 | from ui.renaming_rule_window_view import RenamingRuleWindowView 12 | from ui.renaming_rule_window_controller import RenamingRuleWindowController 13 | 14 | # load languages and preferences 15 | utils.load_languages() 16 | 17 | app = QApplication(sys.argv) 18 | # main_window_controller = MainWindowController() 19 | view = RenamingRuleWindowController() 20 | app.exec_() 21 | 22 | except: 23 | import traceback 24 | 25 | # TODO 26 | # import exceptionhandler 27 | # exceptionhandler.save_exception() 28 | traceback.print_exc() 29 | -------------------------------------------------------------------------------- /src/application.py: -------------------------------------------------------------------------------- 1 | 2 | NAME = "ALMoviesRenamer" 3 | VERSION = "6" -------------------------------------------------------------------------------- /src/exceptionhandler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | import application 3 | 4 | __author__ = "Alberto Malagoli" 5 | 6 | from PyQt4.QtCore import QT_VERSION_STR, PYQT_VERSION_STR 7 | import imdb 8 | import locale 9 | import os.path 10 | import time 11 | import traceback 12 | import platform 13 | import urllib 14 | import urllib2 15 | import utils 16 | 17 | # directory used to store logs 18 | LOG_PATH = os.path.abspath("log") 19 | 20 | def save_exception(): 21 | """ 22 | collect data on exceptions, and save them on file and to a web service 23 | """ 24 | 25 | # current time 26 | time_info = time.strftime("%Y-%m-%d, %H:%M:%S") 27 | # x86 or x64 28 | architecture_info = platform.architecture()[0] 29 | # OS 30 | os_info = platform.platform() 31 | # language 32 | locale_info = locale.getdefaultlocale()[0] 33 | # program version 34 | program_info = application.VERSION 35 | # python version 36 | python_info = platform.python_version() 37 | # qt version 38 | qt_info = str(QT_VERSION_STR) 39 | # pyqt version 40 | pyqt_info = str(PYQT_VERSION_STR) 41 | try: 42 | import sipconfig 43 | # sip version 44 | sip_info = sipconfig.Configuration().sip_version_str 45 | except ImportError: 46 | sip_info = '' 47 | # imdbpy version 48 | imdbpy_info = str(imdb.VERSION) 49 | # enzyme version 50 | enzyme_info = "0.2" 51 | # error data 52 | error_info = traceback.format_exc() 53 | # separator 54 | separator = '-' * 60 55 | # create info list 56 | info = [ 57 | separator, 58 | time_info, 59 | separator, 60 | "Architecture: " + architecture_info, 61 | "Operative System: " + os_info, 62 | "Locale: " + locale_info, 63 | application.NAME + ": " + program_info, 64 | "Python: " + python_info, 65 | "Qt: " + qt_info, 66 | "PyQt: " + pyqt_info, 67 | "sip: " + sip_info, 68 | "IMDbPY: " + imdbpy_info, 69 | "enzyme: " + enzyme_info, 70 | separator, 71 | error_info, 72 | '\n' 73 | ] 74 | # create a string 75 | info_str = '\n'.join(info) 76 | 77 | save_on_file_(info_str) 78 | send_to_ws_(info_str) 79 | 80 | def save_on_file_(info_str): 81 | """ 82 | save collected exceptions data on file 83 | """ 84 | 85 | if not os.path.isdir(LOG_PATH): os.mkdir(LOG_PATH) 86 | 87 | log_file_name = time.strftime("%Y-%m-%d") + ".log" 88 | log_file = os.path.join(LOG_PATH, log_file_name) 89 | 90 | try: 91 | with open(log_file, "a") as f: 92 | f.write(info_str) 93 | except IOError: 94 | pass 95 | 96 | def send_to_ws_(info_str): 97 | """ 98 | send collected exceptions data to a web service 99 | """ 100 | 101 | url = "http://almoviesrenamer.appspot.com/exceptions" 102 | values = { 103 | 'exception' : info_str 104 | } 105 | data = urllib.urlencode(values) 106 | # call web service 107 | f = urllib2.urlopen(url, data) 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/guess.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import re 3 | 4 | import utils 5 | from movie import Movie 6 | 7 | # black words in file names 8 | blackwords = [ 9 | # video type 10 | 'DVDRip', 'HD-DVD', 'HDDVD', 'HDDVDRip', 'BluRay', 'Blu-ray', 'BDRip', 'BRRip', 11 | 'HDRip', 'DVD', 'DVDivX', 'HDTV', 'DVB', 'DVBRip', 'PDTV', 'WEBRip', 'DVDSCR', 12 | 'Screener', 'VHS', 'VIDEO_TS', 13 | # screen 14 | '720p', '720', 15 | # video codec 16 | 'XviD', 'DivX', 'x264', 'h264', 'Rv10', 17 | # audio codec 18 | 'AC3', 'DTS', 'He-AAC', 'AAC-He', 'AAC', '5.1', 19 | # ripper teams 20 | 'ESiR', 'WAF', 'SEPTiC', '[XCT]', 'iNT', 'PUKKA', 'CHD', 'ViTE', 'TLF', 21 | 'DEiTY', 'FLAiTE', 'MDX', 'GM4F', 'DVL', 'SVD', 'iLUMiNADOS', 22 | 'UnSeeN', 'aXXo', 'KLAXXON', 'NoTV', 'ZeaL', 'LOL' 23 | ] 24 | 25 | 26 | def guess_info(title): 27 | """ 28 | given a title, tries to guess as much information as possible. 29 | 30 | guessed information: 31 | title, year, language, part 32 | """ 33 | 34 | # create info dictionary 35 | info = dict() 36 | # guess year 37 | title, year = guess_year_(title) 38 | if year != None: 39 | info.update({Movie.YEAR: year}) 40 | # guess language 41 | title, language = guess_language_(title) 42 | if language != None: 43 | info.update({Movie.LANGUAGE: language}) 44 | # guess part 45 | title, part = guess_part_(title) 46 | if part != None: 47 | info.update({Movie.PART: part}) 48 | # guess subtitles 49 | title, subtitles = guess_subtitles_(title) 50 | if subtitles != None: 51 | info.update({Movie.SUBTITLES: subtitles}) 52 | # clean title 53 | title = clean_title_(title) 54 | info.update({Movie.TITLE: title}) 55 | # return guessed information 56 | return info 57 | 58 | 59 | def guess_year_(title): 60 | """ 61 | looks for year patterns, and return found year 62 | 63 | note this only looks for valid production years, that is between 1920 64 | and now + 5 years, so for instance 2000 would be returned as a valid 65 | year but 1492 would not 66 | """ 67 | 68 | year = None 69 | # search for year pattern (4 consequent digit) 70 | match = re.search(r'[0-9]{4}', title) 71 | # if found, check if year is between 1920 and now + 5 years 72 | if match \ 73 | and 1920 < int(match.group(0)) < datetime.date.today().year + 5: 74 | year = match.group(0) 75 | # remove year from title 76 | title = title[:match.start()] + title[match.end():] 77 | return title, year 78 | 79 | 80 | def guess_language_(title): 81 | """ 82 | guess movie language, looking for ISO language representation in title 83 | """ 84 | 85 | language = None 86 | match = re.search(r'\b([a-zA-Z]{3})\b', title) 87 | if match: 88 | # get corresponding language, given 3-letters ISO language code found 89 | language = utils.alpha3_to_language(match.group(0)) 90 | # language detected 91 | if language != None: 92 | # remove language from title 93 | title = title[:match.start()] + title[match.end():] 94 | return title, language 95 | 96 | 97 | def guess_subtitles_(title): 98 | """ 99 | guess subtitles subtitles, looking for ISO subtitles representation in title 100 | """ 101 | 102 | subtitles = None 103 | match = re.search(r'(?:[^a-zA-Z0-9]sub )([a-zA-Z]{3})(?:[^a-zA-Z0-9])', title) 104 | if match: 105 | # get corresponding subtitles, given 3-letters ISO subtitles code found 106 | subtitles = utils.alpha3_to_language(match.group(1)) 107 | # subtitles detected 108 | if subtitles != None: 109 | # remove subtitles from title 110 | title = title[:match.start() + 1] + title[match.end() - 1:] 111 | return title, subtitles 112 | 113 | 114 | def guess_part_(title): 115 | """ 116 | guess movie part, e.g. CD1 -> 1 117 | """ 118 | 119 | part = None 120 | # search part, which can be like, for example, disk1 or disk 1 121 | match = re.search(r'(?:cd|disk|part[ ]?)(\d)', title, re.IGNORECASE) 122 | if match: 123 | # get part number 124 | part = match.group(1) 125 | # remove part from title 126 | title = title[:match.start()] + title[match.end():] 127 | return title, part 128 | 129 | 130 | def clean_title_(title): 131 | # remove everything inside parenthesis 132 | title = re.sub('[([{].*?[)\]}]', ' ', title) 133 | # replace dots, underscores and dashes with spaces 134 | title = re.sub(r'[._-]', ' ', title) 135 | stitle = title.split() 136 | title = [] 137 | # loop on name 138 | # keep only words which are not black words 139 | for word in stitle: 140 | is_not_a_blackword = True 141 | for blackword in blackwords: 142 | if word.lower() == blackword.lower(): 143 | is_not_a_blackword = False 144 | break 145 | if is_not_a_blackword: 146 | title.append(word) 147 | else: 148 | break 149 | title = ' '.join(title) 150 | return title 151 | -------------------------------------------------------------------------------- /src/icons/brand.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/src/icons/brand.ico -------------------------------------------------------------------------------- /src/icons/brand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/src/icons/brand.png -------------------------------------------------------------------------------- /src/icons/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/src/icons/cross.png -------------------------------------------------------------------------------- /src/icons/eraser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/src/icons/eraser.png -------------------------------------------------------------------------------- /src/icons/exclamation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/src/icons/exclamation.png -------------------------------------------------------------------------------- /src/icons/film.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/src/icons/film.png -------------------------------------------------------------------------------- /src/icons/information.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/src/icons/information.png -------------------------------------------------------------------------------- /src/icons/magnifier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/src/icons/magnifier.png -------------------------------------------------------------------------------- /src/icons/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/src/icons/minus.png -------------------------------------------------------------------------------- /src/icons/movie_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/src/icons/movie_add.png -------------------------------------------------------------------------------- /src/icons/movie_erase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/src/icons/movie_erase.png -------------------------------------------------------------------------------- /src/icons/movie_remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/src/icons/movie_remove.png -------------------------------------------------------------------------------- /src/icons/movies_from_folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/src/icons/movies_from_folder.png -------------------------------------------------------------------------------- /src/icons/pencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/src/icons/pencil.png -------------------------------------------------------------------------------- /src/icons/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/src/icons/plus.png -------------------------------------------------------------------------------- /src/icons/tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/src/icons/tag.png -------------------------------------------------------------------------------- /src/icons/tick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/src/icons/tick.png -------------------------------------------------------------------------------- /src/icons/wrench-screwdriver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albemala/almoviesrenamer/bb73ef5bf2757bc3d6008cf5fdd142cd88ebb51a/src/icons/wrench-screwdriver.png -------------------------------------------------------------------------------- /src/languages.txt: -------------------------------------------------------------------------------- 1 | Swahili|swa|Tanzania 2 | Swedish|swe|Sweden 3 | Lithuanian|lit|Lithuania 4 | Estonian|est|Estonia 5 | Turkish|tur|Azerbaijan;Turkey 6 | Romanian|ron|Romania 7 | Samoan|smo|Samoa 8 | Slovenian|slv|Slovenia 9 | Tok Pisin|tpi|Papua New Guinea 10 | Palauan|pau|Palau 11 | Macedonian|mkd|Macedonia 12 | Icelandic|isl|Iceland 13 | Hindi|hin|India 14 | Dutch|nld|Netherlands;Belgium;Suriname 15 | Norwegian|nor|Norway 16 | Marshallese|mah|Marshall Islands 17 | Korean|kor|Korea, North;Korea, South 18 | Danish|dan|Denmark 19 | Bulgarian|bul|Bulgaria 20 | Lao|lao|Laos 21 | Somali|som|Somalia 22 | Filipino|fil|Philippines 23 | Ukrainian|ukr|Ukraine 24 | Bosnian|bos|Bosnia and Herzegovina 25 | Georgian|kat|Georgia 26 | Vietnamese|vie|Vietnam 27 | Malay|msa|Brunei 28 | French|fra|Cameroon;Burkina Faso;Dominica;Gabon;Monaco;France;Benin;Togo;Central African Republic;Mali;Senegal;Niger;Congo, Republic of;Guinea;Congo, Democratic Republic of the;Luxembourg;Haiti;Chad;Burundi;Madagascar;Cote d'Ivoire;Comoros 29 | Tetum|tet|East Timor 30 | Catalan|cat|Andorra 31 | Armenian|hye|Armenia 32 | Russian|rus|Russia 33 | Tajik|tgk|Tajikistan 34 | Thai|tha|Thailand 35 | Croatian|hrv|Croatia 36 | Turkmen|tuk|Turkmenistan 37 | Nepali|nep|Nepal 38 | Finnish|fin|Finland 39 | Uzbek|uzb|Uzbekistan 40 | Albanian|sqi|Albania;Kosovo 41 | Hebrew|heb|Israel 42 | Khmer|khm|Cambodia 43 | Greek|ell|Cyprus;Greece 44 | Burmese|mya|Myanmar 45 | Latvian|lav|Latvia 46 | English|eng|Canada;Swaziland;Ghana;St. Lucia;Liberia;Zambia;Bahamas;New Zealand;Jamaica;Lesotho;Kenya;Fiji;Solomon Islands;Ireland;United States;Australia;South Africa;St. Vincent and the Grenadines;Uganda;Nigeria;USA;St. Kitts and Nevis;Kiribati;Belize;Sierra Leone;Gambia;Micronesia;Grenada;Antigua and Barbuda;Barbados;Malta;Zimbabwe;UK;Trinidad and Tobago;South Sudan;Guyana;Botswana;United Kingdom;Namibia 47 | Serbian|srp|Serbia 48 | Afar|aar|Eritrea 49 | Italian|ita|San Marino;Vatican City;Italy 50 | Portuguese|por|Cape Verde;Angola;Portugal;Mozambique;Brazil;Guinea-Bissau;Sao Tome and Principe 51 | Chinese|zho|China;Taiwan 52 | German|deu|Germany;Liechtenstein;West Germany;Switzerland;East Germany;Austria 53 | Bislama|bis|Vanuatu 54 | Japanese|jpn|Japan 55 | Kinyarwanda|kin|Rwanda 56 | Amharic|amh|Ethiopia 57 | Czech|ces|Czech Republic;Czechoslovakia 58 | Persian|fas|Afghanistan;Iran 59 | Slovak|slk|Slovakia 60 | Mongolian|mon|Mongolia 61 | Dzongkha|dzo|Bhutan 62 | Spanish|spa|Argentina;Bolivia;Guatemala;Paraguay;Uruguay;Peru;Cuba;Dominican Republic;Panama;Costa Rica;Ecuador;El Salvador;Chile;Equatorial Guinea;Spain;Colombia;Nicaragua;Venezuela;Honduras;Mexico 63 | Urdu|urd|Pakistan 64 | Polish|pol|Poland 65 | Arabic|ara|Saudi Arabia;Kuwait;Oman;Yemen;United Arab Emirates;Bahrain;Libya;Palestinian State;Qatar;Algeria;Morocco;Lebanon;Tunisia;Djibouti;Sudan;Jordan;Mauritania;Iraq;Syria;Egypt 66 | Sinhala|sin|Sri Lanka 67 | Hungarian|hun|Hungary -------------------------------------------------------------------------------- /src/main_window.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import threading 4 | from PyQt5.QtCore import Qt, pyqtSignal, PYQT_VERSION_STR, QUrl 5 | from PyQt5.QtGui import QBrush, QDesktopServices 6 | from PyQt5.QtWidgets import QMainWindow, QApplication, QFileDialog, QTableWidgetItem, QMessageBox 7 | from PyQt5.uic import loadUi 8 | import application 9 | from movie import Movie 10 | from preferences import preferences 11 | from preferences_dialog import PreferencesDialog 12 | from stats_agreement_dialog import StatsAgreementDialog 13 | import utils 14 | 15 | __author__ = "Alberto Malagoli" 16 | 17 | 18 | class MainWindow(QMainWindow): 19 | VIDEO_EXTENSIONS = [".3g2", ".3gp", ".asf", ".asx", ".avi", ".flv", 20 | ".m2ts", ".mkv", ".mov", ".mp4", ".mpg", ".mpeg", 21 | ".rm", ".swf", ".vob", ".wmv"] 22 | 23 | load_movies_finished = pyqtSignal() 24 | search_title_finished = pyqtSignal() 25 | 26 | def __init__(self): 27 | super().__init__() 28 | 29 | # TODO 30 | # # check internet connection 31 | # self.check_connection() 32 | # # check for new program version 33 | # self.check_new_version() 34 | 35 | # variables 36 | # stores movies objects 37 | self._movies = [] 38 | 39 | # TODO 40 | # # show stats agreement dialog 41 | # self.show_stats_agreement() 42 | 43 | # load GUI 44 | self._ui = loadUi("main_window.ui", self) 45 | # create SettingsDialog 46 | self._ui.preferences_dialog = PreferencesDialog(self) 47 | # create RenamingRuleDialog 48 | # self._ui.renaming_rule_dialog = RenamingRuleDialog(self, self._ui.preferences_dialog) 49 | # set some GUI parameters 50 | self.setWindowTitle(application.NAME) 51 | self._ui.panel_loading.setVisible(False) 52 | self._ui.stack_movie.setVisible(False) 53 | self._ui.table_movies.resizeColumnToContents(0) 54 | 55 | # signals connection 56 | # MENU Movies 57 | self._ui.action_add_movies.triggered.connect(self.add_movies) 58 | self._ui.action_add_all_movies_in_folder.triggered.connect(self.add_movies_in_folder) 59 | self._ui.action_add_all_movies_in_folder_subfolders.triggered.connect(self.add_movies_in_folder_and_subfolders) 60 | self._ui.action_remove_selected_movies.triggered.connect(self.remove_selected_movies) 61 | self._ui.action_remove_all_movies.triggered.connect(self.remove_all_movies) 62 | self._ui.action_change_renaming_rule.triggered.connect(self.change_renaming_rule) 63 | self._ui.action_rename_movies.triggered.connect(self.rename_movies) 64 | self.load_movies_finished.connect(self.load_movies_end) 65 | self._ui.text_search_title.returnPressed.connect(self.search_new_title) 66 | self._ui.button_search_title.clicked.connect(self.search_new_title) 67 | self.search_title_finished.connect(self.search_title_end) 68 | # MENU Program 69 | self._ui.action_preferences.triggered.connect(self.show_preferences) 70 | self._ui.action_about.triggered.connect(self.show_about) 71 | # TABLE movies 72 | self._ui.table_movies.itemSelectionChanged.connect(self.movies_selection_changed) 73 | self._ui.table_movies.itemDoubleClicked.connect(self.movie_double_clicked) 74 | self._ui.table_movies.addAction(self.action_copy_title) 75 | self._ui.table_movies.addAction(self.action_open_containing_folder) 76 | self._ui.action_copy_title.triggered.connect(self.copy_title) 77 | self._ui.action_open_containing_folder.triggered.connect(self.open_containing_folder) 78 | # STACK movie 79 | self._ui.table_others_info.itemSelectionChanged.connect(self.alternative_movies_selection_changed) 80 | self.show() 81 | 82 | def show_stats_agreement(self): 83 | """ 84 | shows usage statistics agreement dialog 85 | """ 86 | 87 | # get if this is the first time user opens the program 88 | first_time = preferences.get_first_time_opening() 89 | if first_time: 90 | # create agreement dialog 91 | stats_agreement_dialog = StatsAgreementDialog(self) 92 | # show it 93 | stats_agreement_dialog.exec_() 94 | # nex time user will open the program, don't show that dialog 95 | preferences.set_first_time_opening(False) 96 | 97 | # --------------------------------- SLOTS ---------------------------------- 98 | 99 | # MENU Movies 100 | 101 | def add_movies(self): 102 | """ 103 | select video files from file system using a FileDialog, 104 | then creates corresponding movie objects 105 | 106 | these movies will populate movie table 107 | 108 | get information from selected files 109 | """ 110 | 111 | # create a filter, only video files can be selected 112 | video_filter = "Video (*{0})".format(" *".join(self.VIDEO_EXTENSIONS)) 113 | # dialog title 114 | title = "Select movies you want to rename..." 115 | # select video files from file system 116 | open_files_result = QFileDialog.getOpenFileNames(self, title, preferences.get_last_visited_directory(), 117 | video_filter) 118 | files_paths = open_files_result[0] 119 | # if at least one file has been selected 120 | if len(files_paths) > 0: 121 | self.load_movies(files_paths) 122 | 123 | def add_movies_in_folder(self): 124 | """ 125 | select all video files from a selected folder using a FileDialog, 126 | then creates corresponding movie objects 127 | 128 | these movies will populate movie table 129 | 130 | get information from selected files 131 | """ 132 | 133 | # dialog title 134 | title = "Select a folder containing movies..." 135 | # select folder from file system 136 | folder_path = QFileDialog.getExistingDirectory(self, title, preferences.get_last_visited_directory()) 137 | # if a directory has been selected 138 | if folder_path != "": 139 | files_paths = [] 140 | # for each entry (files + folders) in selected folder 141 | for entry in os.listdir(folder_path): 142 | self.add_file_to_list_if_is_video(folder_path, entry, files_paths) 143 | 144 | self.load_movies(files_paths) 145 | 146 | def add_movies_in_folder_and_subfolders(self): 147 | """ 148 | select all video files from a selected folder and subfolders using a FileDialog, 149 | then creates corresponding movie objects 150 | 151 | these movies will populate movie table 152 | 153 | get information from selected files 154 | """ 155 | 156 | # dialog title 157 | title = "Select a folder containing movies..." 158 | # select folder from file system 159 | folder_path = QFileDialog.getExistingDirectory(self, title, preferences.get_last_visited_directory()) 160 | # if a directory has been selected 161 | if folder_path != "": 162 | files_paths = [] 163 | # walk on chosen directory, and loop on files and directories 164 | for root, dirs, files in os.walk(folder_path): 165 | # for each file 166 | for name in files: 167 | self.add_file_to_list_if_is_video(root, name, files_paths) 168 | 169 | self.load_movies(files_paths) 170 | 171 | def add_file_to_list_if_is_video(self, path, name, files): 172 | entry = os.path.join(path, name) 173 | # select only video files 174 | extension = os.path.splitext(name)[1].lower() 175 | if os.path.isfile(entry) and extension in self.VIDEO_EXTENSIONS: 176 | # save entry with path 177 | files.append(entry) 178 | 179 | def load_movies(self, files_paths): 180 | """ 181 | given a list of file paths, creates a movie object for each 182 | file, get info from it, and populate movies table 183 | """ 184 | 185 | # takes first selected file and get the file path, use it as the last visited directory 186 | first_file_path = files_paths[0] 187 | file_path = os.path.split(first_file_path)[0] 188 | last_visited_directory = os.path.normpath(file_path) 189 | # save it in settings 190 | preferences.set_last_visited_directory(last_visited_directory) 191 | # disable gui elements which cannot be used during loading 192 | self.set_gui_enabled_load_movies(False) 193 | # show loading panel 194 | self._ui.panel_loading.setVisible(True) 195 | # start loading thread 196 | threading.Thread(target=self.load_movies_run, args=(files_paths,)).start() 197 | 198 | def load_movies_run(self, file_paths): 199 | # loop on file paths 200 | for file_path in file_paths: 201 | # set loading label text, show current file name 202 | file_name = os.path.split(file_path)[1] 203 | self._ui.label_loading.setText("Getting information from {0}".format(file_name)) 204 | movie = self.create_movie(file_path) 205 | # add movie to list 206 | self._movies.append(movie) 207 | self.insert_movie_in_table_view(movie) 208 | self.load_movies_finished.emit() 209 | 210 | def create_movie(self, file_path): 211 | # create a new movie object 212 | movie = Movie() 213 | movie.fill_with_file(file_path) 214 | movie_guessed_info = movie.get_guessed_info() 215 | movie.fetch_tmdb_info(movie_guessed_info.get_title(), movie_guessed_info.get_year(), 216 | movie_guessed_info.get_language()) 217 | # generate new movie name based on renaming rule 218 | movie.generate_new_name(preferences.get_renaming_rule()) 219 | return movie 220 | 221 | def insert_movie_in_table_view(self, movie): 222 | # insert a new row in movie table 223 | self._ui.table_movies.insertRow(self._ui.table_movies.rowCount()) 224 | row = self._ui.table_movies.rowCount() - 1 225 | # create a table item with original movie file name 226 | item_original_name = self.create_table_view_movie_item(movie.get_file_info().get_original_file_name()) 227 | self._ui.table_movies.setItem(row, 0, item_original_name) 228 | # create a table item with new movie file name 229 | item_new_name = self.create_table_view_movie_item(movie.get_file_info().get_renamed_file_name()) 230 | self._ui.table_movies.setItem(row, 1, item_new_name) 231 | 232 | def create_table_view_movie_item(self, text): 233 | item_original_name = QTableWidgetItem(text) 234 | item_original_name.setForeground(QBrush(Qt.black)) 235 | return item_original_name 236 | 237 | def load_movies_end(self): 238 | # re-enable gui elements 239 | self.set_gui_enabled_load_movies(True) 240 | # auto resize table columns 241 | self._ui.table_movies.resizeColumnToContents(0) 242 | # hide loading panel 243 | self._ui.panel_loading.setVisible(False) 244 | # play a sound 245 | QApplication.beep() 246 | 247 | def set_gui_enabled_load_movies(self, enabled): 248 | # TODO check 249 | # set enabled property on actions 250 | self._ui.action_add_movies.setEnabled(enabled) 251 | self._ui.action_add_all_movies_in_folder.setEnabled(enabled) 252 | self._ui.action_add_all_movies_in_folder_subfolders.setEnabled(enabled) 253 | self._ui.action_remove_selected_movies.setEnabled(enabled) 254 | self._ui.action_remove_all_movies.setEnabled(enabled) 255 | self._ui.action_change_renaming_rule.setEnabled(enabled) 256 | self._ui.action_rename_movies.setEnabled(enabled) 257 | self._ui.action_preferences.setEnabled(enabled) 258 | if not enabled: 259 | # clear table selection (and hide movie panel, if visible) 260 | self._ui.table_movies.selectionModel().clear() 261 | # set enabled property on table 262 | self._ui.table_movies.setEnabled(enabled) 263 | 264 | def remove_selected_movies(self): 265 | """ 266 | removes selected movies from movies table 267 | """ 268 | 269 | # get selected items 270 | selected_items = self._ui.table_movies.selectionModel().selectedRows() 271 | # loop on items 272 | for item in reversed(selected_items): 273 | # get item row 274 | row = item.row() 275 | # first remove items from table 276 | self._ui.table_movies.takeItem(row, 0) 277 | self._ui.table_movies.takeItem(row, 1) 278 | # them remove corresponding row 279 | self._ui.table_movies.removeRow(row) 280 | # delete movie item 281 | del self._movies[row] 282 | 283 | def remove_all_movies(self): 284 | """ 285 | removes all movies from movies table 286 | """ 287 | 288 | del self._movies[:] 289 | # clear table contents 290 | self._ui.table_movies.clearContents() 291 | # remove all rows 292 | self._ui.table_movies.setRowCount(0) 293 | 294 | def change_renaming_rule(self): 295 | """ 296 | show renaming rule dialog 297 | """ 298 | 299 | self._ui.table_movies.clearSelection() 300 | # show renaming rule dialog 301 | self._ui.renaming_rule_dialog.exec_() 302 | renaming_rule = preferences.get_renaming_rule() 303 | # loop on movies 304 | for i in range(len(self._movies)): 305 | movie = self._movies[i] 306 | # generate new movie name based on new renaming rule 307 | movie.generate_new_name(renaming_rule) 308 | # set "before renaming state", because after renaming a movie 309 | # can be renamed a second time, after having changed the rule 310 | movie.set_renaming_state(Movie.STATE_BEFORE_RENAMING) 311 | self._ui.table_movies.item(i, 1).setForeground(QBrush(Qt.black)) 312 | self._ui.table_movies.item(i, 1).setText(movie.get_renamed_file_name()) 313 | 314 | def rename_movies(self): 315 | """ 316 | rename files with new name 317 | """ 318 | 319 | self._ui.table_movies.clearSelection() 320 | # loop on movies 321 | for i in range(len(self._movies)): 322 | movie = self._movies[i] 323 | # check if new title is a valid file name 324 | if movie.check_and_clean_new_name(): 325 | # set "new name" table item with new movie name 326 | self._ui.table_movies.item(i, 1).setText(movie.get_renamed_file_name()) 327 | try: 328 | # rename file 329 | os.rename(movie.get_absolute_original_file_path(), movie.get_absolute_renamed_file_path()) 330 | except OSError as e: 331 | # set state and renaming error 332 | movie.set_renaming_state(Movie.STATE_RENAMING_ERROR, e.strerror) 333 | # paint "new name" table item with red 334 | self._ui.table_movies.item(i, 1).setForeground(QBrush(Qt.red)) 335 | else: 336 | # correctly renamed 337 | movie.set_renaming_state(Movie.STATE_RENAMED) 338 | # set "original name" table item with new movie name 339 | self._ui.table_movies.item(i, 0).setText(movie.get_renamed_file_name()) 340 | # paint "new name" table item with green 341 | self._ui.table_movies.item(i, 1).setForeground(QBrush(Qt.darkGreen)) 342 | 343 | # MENU Program 344 | 345 | def show_preferences(self): 346 | # show renaming rule dialog 347 | self._ui.preferences_dialog.exec_() 348 | renaming_rule = preferences.get_renaming_rule() 349 | # loop on movies 350 | for i in range(len(self._movies)): 351 | movie = self._movies[i] 352 | # generate new movie name based on new renaming rule 353 | movie.generate_new_name(renaming_rule) 354 | # set "before renaming state", because after renaming a movie 355 | # can be renamed a second time, after having changed the rule 356 | movie.set_renaming_state(Movie.STATE_BEFORE_RENAMING) 357 | self._ui.table_movies.item(i, 1).setForeground(QBrush(Qt.black)) 358 | self._ui.table_movies.item(i, 1).setText(movie.get_renamed_file_name()) 359 | 360 | def show_about(self): 361 | """ 362 | shows an about dialog, with info on program 363 | """ 364 | 365 | # message shown on about dialog 366 | msg = """ 367 |
368 | {0} 369 |
370 |
371 | Version: {1}
372 | License: GNU General Public License version 3 (GPLv3)
373 |
375 | Programmed by: Alberto Malagoli
376 | Email: albemala@gmail.com
377 |
379 | Libraries: 380 |
399 | Thanks to: 400 |
440 | """ 441 | + selected_movie.get_renaming_error() + 442 | """ 443 |
444 | """) 445 | elif selected_movie.get_renaming_state() == Movie.STATE_BEFORE_RENAMING: 446 | self.populate_movie_panel() 447 | self._ui.stack_search_title.setCurrentIndex(0) 448 | self._ui.text_search_title.clear() 449 | 450 | # set panel visible 451 | self._ui.stack_movie.setVisible(True) 452 | 453 | def movie_double_clicked(self, item): 454 | """ 455 | when a movie item is double clicked in table, 456 | it opens operative system file manager 457 | with file location on disk 458 | """ 459 | 460 | movie = self._movies[item.row()] 461 | path = movie.get_directory_path() 462 | self.open_path(path) 463 | 464 | def open_containing_folder(self): 465 | movie = self.__get_selected_movie() 466 | if movie is not None: 467 | path = movie.get_directory_path() 468 | self.open_path(path) 469 | 470 | def open_path(self, path): 471 | """ 472 | opens a path using the operative system file manager/explorer 473 | """ 474 | QDesktopServices.openUrl(self, QUrl('file:///' + path)) 475 | 476 | def copy_title(self): 477 | """ 478 | copy selected movie title in movie table 479 | into clipboard 480 | """ 481 | 482 | movie = self.__get_selected_movie() 483 | if movie != None: 484 | clipboard = QApplication.clipboard() 485 | clipboard.setText(movie.get_original_file_name()) 486 | 487 | def populate_movie_panel(self): 488 | movie = self.__get_selected_movie() 489 | 490 | self._ui.label_title.setText(movie.get_title()) 491 | self._ui.label_original_title.setText(movie.get_original_title()) 492 | self._ui.label_year.setText(movie.get_year()) 493 | self._ui.label_director.setText(movie.get_director()) 494 | self._ui.label_duration.setText(movie.get_duration()) 495 | language = movie.get_language() 496 | if movie.get_subtitle_language() != "": 497 | language += " (subtitled " + movie.get_subtitle_language() + ")" 498 | self._ui.label_language.setText(language) 499 | 500 | # clear table contents 501 | self._ui.table_others_info.clearContents() 502 | # remove all rows 503 | self._ui.table_others_info.setRowCount(0) 504 | # TODO 505 | # for other_info in movie_info.others_info(): 506 | # title = other_info[0] 507 | # language = other_info[1] 508 | # # insert a new row in movie table 509 | # self._ui.table_others_info.insertRow(self._ui.table_others_info.rowCount()) 510 | # # create a table item with original movie file name 511 | # item_title = QTableWidgetItem(title) 512 | # self._ui.table_others_info.setItem(self._ui.table_others_info.rowCount() - 1, 0, item_title) 513 | # item_language = QTableWidgetItem(language) 514 | # self._ui.table_others_info.setItem(self._ui.table_others_info.rowCount() - 1, 1, item_language) 515 | # auto resize table columns 516 | self._ui.table_others_info.resizeColumnToContents(0) 517 | 518 | # PANEL movie 519 | 520 | def alternative_movies_selection_changed(self): 521 | selected_info = self._ui.table_others_info.selectedItems() 522 | if len(selected_info) > 0: 523 | info_index = selected_info[0].row() 524 | movie = self.__get_selected_movie() 525 | movie.set_movie(info_index) 526 | renaming_rule = preferences.get_renaming_rule() 527 | # generate new movie name based on renaming rule 528 | movie.generate_new_name(renaming_rule) 529 | # create a table item with new movie file name 530 | item_new_name = QTableWidgetItem(movie.get_renamed_file_name()) 531 | selected_movie = self._ui.table_movies.selectedItems()[0] 532 | # store first selected movie 533 | movie_index = selected_movie.row() 534 | self._ui.table_movies.setItem(movie_index, 1, item_new_name) 535 | # update labels in movie panel 536 | self._ui.label_title.setText(movie.get_title()) 537 | self._ui.label_original_title.setText(movie.get_original_title()) 538 | self._ui.label_year.setText(movie.get_year()) 539 | self._ui.label_director.setText(movie.get_directors()) 540 | self._ui.label_duration.setText(movie.get_duration()) 541 | self._ui.label_language.setText(movie.get_language()) 542 | 543 | def search_new_title(self): 544 | # get title to look for 545 | title = str(self._ui.text_search_title.text()) 546 | # do not start searching if textTitleSearch is empty 547 | if title.strip() == '': 548 | return 549 | # set gui elements disabled 550 | self.set_gui_enabled_search_title(False) 551 | self._ui.label_searching.setText("Searching {0}...".format(title)) 552 | # show searching panel 553 | self._ui.stack_search_title.setCurrentIndex(1) 554 | # start searching thread 555 | threading.Thread(target=self.search_title_run, args=(title,)).start() 556 | 557 | def search_title_run(self, title): 558 | """ 559 | thread used for movie title searching 560 | """ 561 | 562 | self.__get_selected_movie().search_new_title(title) 563 | # emit signal 564 | self.search_title_finished.emit() 565 | 566 | def search_title_end(self): 567 | """ 568 | used when movie title searching finishes (thread returns) 569 | """ 570 | 571 | # re-enable gui elements 572 | self.set_gui_enabled_search_title(True) 573 | renaming_rule = preferences.get_renaming_rule() 574 | # generate new movie name based on renaming rule 575 | movie = self.__get_selected_movie() 576 | movie.generate_new_name(renaming_rule) 577 | # create a table item with new movie file name 578 | item_new_name = QTableWidgetItem(movie.get_renamed_file_name()) 579 | selected_movie = self._ui.table_movies.selectedItems()[0] 580 | # store first selected movie 581 | movie_index = selected_movie.row() 582 | self._ui.table_movies.setItem(movie_index, 1, item_new_name) 583 | self.populate_movie_panel() 584 | self._ui.stack_search_title.setCurrentIndex(0) 585 | 586 | def set_gui_enabled_search_title(self, enabled): 587 | # set enabled property on actions 588 | self._ui.action_add_movies.setEnabled(enabled) 589 | self._ui.action_add_all_movies_in_folder.setEnabled(enabled) 590 | self._ui.action_add_all_movies_in_folder_subfolders.setEnabled(enabled) 591 | self._ui.action_remove_selected_movies.setEnabled(enabled) 592 | self._ui.action_remove_all_movies.setEnabled(enabled) 593 | self._ui.action_change_renaming_rule.setEnabled(enabled) 594 | self._ui.action_rename_movies.setEnabled(enabled) 595 | self._ui.action_preferences.setEnabled(enabled) 596 | # set enabled property on table 597 | self._ui.table_movies.setEnabled(enabled) 598 | 599 | self._ui.table_others_info.setEnabled(enabled) 600 | 601 | def __get_selected_movie(self) -> Movie: 602 | selected_items = self._ui.table_movies.selectedItems() 603 | if len(selected_items) == 0: 604 | return None 605 | index = selected_items[0].row() 606 | selected_movie = self._movies[index] 607 | return selected_movie 608 | -------------------------------------------------------------------------------- /src/movie_file_info.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | __author__ = "Alberto Malagoli" 4 | 5 | 6 | class MovieFileInfo: 7 | def __init__(self): 8 | # file path (only directory) 9 | self._directory_path = "" 10 | # original movie title, before renaming 11 | self._original_file_name = "" 12 | # file extension 13 | self._file_extension = "" 14 | # movie new title (after renaming) 15 | self._renamed_file_name = "" 16 | 17 | def get_original_file_name(self): 18 | return self._original_file_name 19 | 20 | def get_renamed_file_name(self): 21 | return self._renamed_file_name 22 | 23 | def get_absolute_original_file_path(self): 24 | return os.path.join(self._directory_path, self._original_file_name + self._file_extension) 25 | 26 | def get_absolute_renamed_file_path(self): 27 | return os.path.join(self._directory_path, self._renamed_file_name + self._file_extension) 28 | 29 | def get_directory_path(self): 30 | return self._directory_path 31 | 32 | def fill_with_absolute_file_path(self, absolute_file_path): 33 | path, name = os.path.split(absolute_file_path) 34 | name, extension = os.path.splitext(name) 35 | self._directory_path = os.path.normpath(path) 36 | self._original_file_name = name 37 | self._file_extension = extension 38 | -------------------------------------------------------------------------------- /src/movie_guessed_info.py: -------------------------------------------------------------------------------- 1 | import guessit 2 | 3 | __author__ = "Alberto Malagoli" 4 | 5 | 6 | class MovieGuessedInfo: 7 | TITLE = "title" 8 | YEAR = "year" 9 | COUNTRY = "country" 10 | LANGUAGE = "language" 11 | SUBTITLE_LANGUAGE = "subtitleLanguage" 12 | BONUS_TITLE = "bonusTitle" 13 | CD_NUMBER = "cdNumber" 14 | CD_NUMBER_TOTAL = "cdNumberTotal" 15 | EDITION = "edition" 16 | 17 | def __init__(self): 18 | # TODO expose other properties: country, bonus title, cd number total, edition 19 | self.__title = "" 20 | self.__year = "" 21 | # Country(ies) of content. [It seems your internet connection is down (but maybe I'm wrong).
127 | #That program needs access to the internet, to get information about movies, so please check your connection.
128 | #If I'm wrong, sorry for the interruption...
129 | # """) 130 | # icon = QPixmap() 131 | # icon.load('icons/exclamation.png') 132 | # msg_box.setIconPixmap(icon) 133 | # msg_box.exec_() 134 | 135 | # TODO 136 | def check_new_version(self): 137 | pass 138 | # """ 139 | # checks for new program version, and notify in case of 140 | # a newer version 141 | # """ 142 | # 143 | # # create url, with current program version 144 | # url = "http://almoviesrenamer.appspot.com/checknewversion" 145 | # # call web service 146 | # f = urllib2.urlopen(url) 147 | # # read the answer (could be "yes" for new version, or "no") 148 | # version = f.read().rstrip('\n') 149 | # # if there is a new version 150 | # if version != utils.PROGRAM_VERSION: 151 | # title = "New version available" 152 | # msg = """ 153 | #A new version of {0} is available: {1}
154 | # 155 | # """.format(utils.PROGRAM_NAME, version) 156 | # # show a notification dialog, with link to download page 157 | # QMessageBox.information(None, title, msg) 158 | -------------------------------------------------------------------------------- /stats.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | 3 | __author__ = "Alberto Malagoli" 4 | 5 | import requests 6 | 7 | ## dictionaries used to store statistics 8 | # keys: renaming rules, values: occurrences count 9 | rules_dict = dict() 10 | info_dict = dict() 11 | durations = { 12 | '0': 0, # Minutes only (e.g.: 100m) 13 | '1': 0 # Hours and minutes (e.g.: 1h40m) 14 | } 15 | languages = { 16 | '0': 0, # English name (e.g.: English) 17 | '1': 0 # 3-letters (e.g.: ENG) 18 | } 19 | separators = { 20 | '0': 0, # , (comma-space) 21 | '1': 0, # - (space-dash-space) 22 | '2': 0 # (space) 23 | } 24 | 25 | # compose url 26 | url = "http://almoviesrenamer.appspot.com/stats" 27 | # get data from web service 28 | response = requests.get(url) 29 | data = response.content.decode(encoding="UTF-8") 30 | # read statistics, split into lines and remove last (empty) line 31 | stats = data.split('\n')[:-1] 32 | 33 | # for each line 34 | for stat in stats: 35 | # split rules using "&" 36 | stat = stat.split('&') 37 | # current renaming rule is not into dictionary 38 | rule = stat[0] 39 | if rule not in rules_dict: 40 | # add it with count 1 41 | rules_dict.update({rule: 1}) 42 | else: 43 | # increase count 44 | rules_dict[rule] += 1 45 | info = rule.split(".") 46 | for i in info: 47 | if i not in info_dict: 48 | # add it with count 1 49 | info_dict.update({i: 1}) 50 | else: 51 | # increase count 52 | info_dict[i] += 1 53 | # increase count for duration from current statistic 54 | try: 55 | durations[stat[1]] += 1 56 | except: 57 | pass 58 | # increase count for language from current statistic 59 | try: 60 | languages[stat[2]] += 1 61 | except: 62 | pass 63 | # increase count for separator from current statistic 64 | try: 65 | separators[stat[3]] += 1 66 | except: 67 | pass 68 | # sort rules based on occurrences count 69 | rules_dict = sorted(rules_dict.items(), key=lambda x: x[1], reverse=True) 70 | 71 | dashes = 15 72 | # print rules statistics 73 | print("\n" + "-" * dashes + " rules " + "-" * dashes) 74 | for rule in rules_dict: 75 | print(rule[0] + ": " + str(rule[1])) 76 | # print info statistics 77 | print(info_dict) 78 | print("\n" + "-" * dashes + " info " + "-" * dashes) 79 | for info in info_dict: 80 | print(info + ": " + str(info_dict[info])) 81 | # print durations statistics 82 | print("\n" + "-" * dashes + " durations " + "-" * dashes) 83 | print("Minutes only (e.g.: 100m): " + str(durations['0'])) 84 | print("Hours and minutes (e.g.: 1h40m): " + str(durations['1'])) 85 | # print languages statistics 86 | print("\n" + "-" * dashes + " languages " + "-" * dashes) 87 | print("English name (e.g.: English): " + str(languages['0'])) 88 | print("3-letters (e.g.: ENG): " + str(languages['1'])) 89 | # print separators statistics 90 | print("\n" + "-" * dashes + " separators " + "-" * dashes) 91 | print(", (comma-space): " + str(separators['0'])) 92 | print("- (space-dash-space): " + str(separators['1'])) 93 | print(" (space): " + str(separators['2'])) 94 | -------------------------------------------------------------------------------- /upload2google.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2006, 2007 Google Inc. All Rights Reserved. 4 | # Author: danderson@google.com (David Anderson) 5 | # 6 | # Script for uploading files to a Google Code project. 7 | # 8 | # This is intended to be both a useful script for people who want to 9 | # streamline project uploads and a reference implementation for 10 | # uploading files to Google Code projects. 11 | # 12 | # To upload a file to Google Code, you need to provide a path to the 13 | # file on your local machine, a small summary of what the file is, a 14 | # project name, and a valid account that is a member or owner of that 15 | # project. You can optionally provide a list of labels that apply to 16 | # the file. The file will be uploaded under the same name that it has 17 | # in your local filesystem (that is, the "basename" or last path 18 | # component). Run the script with '--help' to get the exact syntax 19 | # and available options. 20 | # 21 | # Note that the upload script requests that you enter your 22 | # googlecode.com password. This is NOT your Gmail account password! 23 | # This is the password you use on googlecode.com for committing to 24 | # Subversion and uploading files. You can find your password by going 25 | # to http://code.google.com/hosting/settings when logged in with your 26 | # Gmail account. If you have already committed to your project's 27 | # Subversion repository, the script will automatically retrieve your 28 | # credentials from there (unless disabled, see the output of '--help' 29 | # for details). 30 | # 31 | # If you are looking at this script as a reference for implementing 32 | # your own Google Code file uploader, then you should take a look at 33 | # the upload() function, which is the meat of the uploader. You 34 | # basically need to build a multipart/form-data POST request with the 35 | # right fields and send it to https://PROJECT.googlecode.com/files . 36 | # Authenticate the request using HTTP Basic authentication, as is 37 | # shown below. 38 | # 39 | # Licensed under the terms of the Apache Software License 2.0: 40 | # http://www.apache.org/licenses/LICENSE-2.0 41 | # 42 | # Questions, comments, feature requests and patches are most welcome. 43 | # Please direct all of these to the Google Code users group: 44 | # http://groups.google.com/group/google-code-hosting 45 | 46 | """Google Code file uploader script.""" 47 | 48 | __author__ = 'danderson@google.com (David Anderson)' 49 | 50 | import httplib 51 | import os.path 52 | import getpass 53 | import base64 54 | import platform 55 | import sys 56 | 57 | 58 | def upload(file, project_name, user_name, password, summary, labels = None): 59 | """Upload a file to a Google Code project's file server. 60 | 61 | Args: 62 | file: The local path to the file. 63 | project_name: The name of your project on Google Code. 64 | user_name: Your Google account name. 65 | password: The googlecode.com password for your account. 66 | Note that this is NOT your global Google Account password! 67 | summary: A small description for the file. 68 | labels: an optional list of label strings with which to tag the file. 69 | 70 | Returns: a tuple: 71 | http_status: 201 if the upload succeeded, something else if an 72 | error occured. 73 | http_reason: The human-readable string associated with http_status 74 | file_url: If the upload succeeded, the URL of the file on Google 75 | Code, None otherwise. 76 | """ 77 | # The login is the user part of user@gmail.com. If the login provided 78 | # is in the full user@domain form, strip it down. 79 | if user_name.endswith('@gmail.com'): 80 | user_name = user_name[:user_name.index('@gmail.com')] 81 | 82 | form_fields = [('summary', summary)] 83 | if labels is not None: 84 | form_fields.extend([('label', l.strip()) for l in labels]) 85 | 86 | content_type, body = encode_upload_request(form_fields, file) 87 | 88 | upload_host = '%s.googlecode.com' % project_name 89 | upload_uri = '/files' 90 | auth_token = base64.b64encode('%s:%s' % (user_name, password)) 91 | headers = { 92 | 'Authorization': 'Basic %s' % auth_token, 93 | 'User-Agent': 'Googlecode.com uploader v0.9.4', 94 | 'Content-Type': content_type, 95 | } 96 | 97 | server = httplib.HTTPSConnection(upload_host) 98 | server.request('POST', upload_uri, body, headers) 99 | resp = server.getresponse() 100 | server.close() 101 | 102 | if resp.status == 201: 103 | location = resp.getheader('Location', None) 104 | else: 105 | location = None 106 | return resp.status, resp.reason, location 107 | 108 | 109 | def encode_upload_request(fields, file_path): 110 | """Encode the given fields and file into a multipart form body. 111 | 112 | fields is a sequence of (name, value) pairs. file is the path of 113 | the file to upload. The file will be uploaded to Google Code with 114 | the same file name. 115 | 116 | Returns: (content_type, body) ready for httplib.HTTP instance 117 | """ 118 | BOUNDARY = '----------Googlecode_boundary_reindeer_flotilla' 119 | CRLF = '\r\n' 120 | 121 | body = [] 122 | 123 | # Add the metadata about the upload first 124 | for key, value in fields: 125 | body.extend( 126 | ['--' + BOUNDARY, 127 | 'Content-Disposition: form-data; name="%s"' % key, 128 | '', 129 | value, 130 | ]) 131 | 132 | # Now add the file itself 133 | file_name = os.path.basename(file_path) 134 | f = open(file_path, 'rb') 135 | file_content = f.read() 136 | f.close() 137 | 138 | body.extend( 139 | ['--' + BOUNDARY, 140 | 'Content-Disposition: form-data; name="filename"; filename="%s"' 141 | % file_name, 142 | # The upload server determines the mime-type, no need to set it. 143 | 'Content-Type: application/octet-stream', 144 | '', 145 | file_content, 146 | ]) 147 | 148 | # Finalize the form body 149 | body.extend(['--' + BOUNDARY + '--', '']) 150 | 151 | return 'multipart/form-data; boundary=%s' % BOUNDARY, CRLF.join(body) 152 | 153 | 154 | def upload_find_auth(file_path, project_name, summary, labels = None, 155 | user_name = None, password = None, tries = 3): 156 | """Find credentials and upload a file to a Google Code project's file server. 157 | 158 | file_path, project_name, summary, and labels are passed as-is to upload. 159 | 160 | Args: 161 | file_path: The local path to the file. 162 | project_name: The name of your project on Google Code. 163 | summary: A small description for the file. 164 | labels: an optional list of label strings with which to tag the file. 165 | config_dir: Path to Subversion configuration directory, 'none', or None. 166 | user_name: Your Google account name. 167 | tries: How many attempts to make. 168 | """ 169 | if user_name is None or password is None: 170 | from netrc import netrc 171 | authenticators = netrc().authenticators("code.google.com") 172 | if authenticators: 173 | if user_name is None: 174 | user_name = authenticators[0] 175 | if password is None: 176 | password = authenticators[2] 177 | 178 | while tries > 0: 179 | if user_name is None: 180 | # Read username if not specified or loaded from svn config, or on 181 | # subsequent tries. 182 | sys.stdout.write('Please enter your googlecode.com username: ') 183 | sys.stdout.flush() 184 | user_name = sys.stdin.readline().rstrip() 185 | if password is None: 186 | # Read password if not loaded from svn config, or on subsequent tries. 187 | print 'Please enter your googlecode.com password.' 188 | print '** Note that this is NOT your Gmail account password! **' 189 | print 'It is the password you use to access Subversion repositories,' 190 | print 'and can be found here: http://code.google.com/hosting/settings' 191 | password = getpass.getpass() 192 | 193 | status, reason, url = upload(file_path, project_name, user_name, password, 194 | summary, labels) 195 | # Returns 403 Forbidden instead of 401 Unauthorized for bad 196 | # credentials as of 2007-07-17. 197 | if status in [httplib.FORBIDDEN, httplib.UNAUTHORIZED]: 198 | # Rest for another try. 199 | user_name = password = None 200 | tries = tries - 1 201 | else: 202 | # We're done. 203 | break 204 | 205 | return status, reason, url 206 | 207 | def upload2google(file_path, project, summary, labels): 208 | 209 | print("file: {0}".format(file_path)) 210 | print("project: {0}".format(project)) 211 | print("summary: {0}".format(summary)) 212 | print("labels: {0}".format(str(labels))) 213 | print("starting file uploading. this may take a while... ({0} KB)" 214 | .format(int(os.path.getsize(file_path) / 1024.0))) 215 | 216 | status, reason, url = upload_find_auth( 217 | file_path, 218 | project, 219 | summary, 220 | labels, 221 | user_name = "albemala", 222 | password = "") 223 | 224 | if url: 225 | print('The file was uploaded successfully.') 226 | print('URL: {0}'.format(url)) 227 | else: 228 | print('An error occurred. Your file was not uploaded.') 229 | print('Google Code upload server said: {0} ({1})'.format(reason, status)) 230 | --------------------------------------------------------------------------------