├── .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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /new icons/ic_check_black_48px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /new icons/ic_clear_black_48px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /new icons/ic_create_black_48px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /new icons/ic_folder_black_48px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /new icons/ic_info_black_48px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /new icons/ic_label_black_48px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /new icons/ic_local_movies_black_48px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /new icons/ic_remove_black_48px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /new icons/ic_settings_black_48px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /new icons/ic_warning_black_48px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /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 |

374 |

375 | Programmed by: Alberto Malagoli
376 | Email: albemala@gmail.com 377 |

378 |

379 | Libraries: 380 |

397 |

398 |

399 | Thanks to: 400 |

412 |

413 | """.format(application.NAME, application.VERSION, platform.python_version(), PYQT_VERSION_STR, 414 | 0 415 | # TODO 416 | # imdb.VERSION 417 | ) 418 | # show the about dialog 419 | QMessageBox.about(self, "About {0}".format(application.NAME), msg) 420 | 421 | # TABLE movies 422 | 423 | def movies_selection_changed(self): 424 | """ 425 | called when selection in table_movies changes, i.e. user selects 426 | different movies from previously selected ones. 427 | """ 428 | selected_movie = self.__get_selected_movie() 429 | # no movies selected 430 | if selected_movie is None: 431 | # hide movie panel 432 | self._ui.stack_movie.setVisible(False) 433 | else: 434 | # set movie panel based on movie state ( 435 | self._ui.stack_movie.setCurrentIndex(selected_movie.get_renaming_state()) 436 | # populate movie panel 437 | if selected_movie.get_renaming_state() == Movie.STATE_RENAMING_ERROR: 438 | self._ui.label_error.setText(""" 439 |

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. [] (This class equals name and iso code) 22 | self.__country = "" 23 | # Language(s) of the audio soundtrack. [] (This class equals name and iso code) 24 | self.__language = "" 25 | # Language(s) of the subtitles. [] (This class equals name and iso code) 26 | self.__subtitle_language = "" 27 | self.__bonus_title = "" 28 | self.__cd_number = "" 29 | self.__cd_number_total = "" 30 | # Special Edition, Collector Edition, Director's cut, Criterion Edition, Deluxe Edition 31 | self.__edition = "" 32 | 33 | def get_title(self) -> str: 34 | return self.__title 35 | 36 | def get_year(self) -> str: 37 | return self.__year 38 | 39 | def get_country(self) -> str: 40 | return self.__country 41 | 42 | def get_language(self) -> str: 43 | return self.__language 44 | 45 | def get_subtitle_language(self) -> str: 46 | return self.__subtitle_language 47 | 48 | def get_bonus_title(self) -> str: 49 | return self.__bonus_title 50 | 51 | def get_cd_number(self) -> str: 52 | return self.__cd_number 53 | 54 | def get_cd_number_total(self) -> str: 55 | return self.__cd_number_total 56 | 57 | def get_edition(self) -> str: 58 | return self.__edition 59 | 60 | def fill_with_absolute_file_path(self, absolute_file_path: str) -> None: 61 | info = guessit.guess_movie_info(absolute_file_path) 62 | # print(info) 63 | 64 | if MovieGuessedInfo.TITLE in info: 65 | self.__title = info[MovieGuessedInfo.TITLE] 66 | if MovieGuessedInfo.YEAR in info: 67 | self.__year = str(info[MovieGuessedInfo.YEAR]) 68 | if MovieGuessedInfo.COUNTRY in info: 69 | self.__country = info[MovieGuessedInfo.COUNTRY][0] 70 | if MovieGuessedInfo.LANGUAGE in info: 71 | self.__language = info[MovieGuessedInfo.LANGUAGE][0].alpha2 72 | if MovieGuessedInfo.SUBTITLE_LANGUAGE in info: 73 | self.__subtitle_language = info[MovieGuessedInfo.SUBTITLE_LANGUAGE][0] 74 | if MovieGuessedInfo.BONUS_TITLE in info: 75 | self.__bonus_title = info[MovieGuessedInfo.BONUS_TITLE] 76 | if MovieGuessedInfo.CD_NUMBER in info: 77 | self.__cd_number = str(info[MovieGuessedInfo.CD_NUMBER]) 78 | if MovieGuessedInfo.CD_NUMBER_TOTAL in info: 79 | self.__cd_number_total = str(info[MovieGuessedInfo.CD_NUMBER_TOTAL]) 80 | if MovieGuessedInfo.EDITION in info: 81 | self.__edition = info[MovieGuessedInfo.EDITION] 82 | -------------------------------------------------------------------------------- /src/movie_info.py: -------------------------------------------------------------------------------- 1 | from movie_guessed_info import MovieGuessedInfo 2 | 3 | __author__ = "Alberto Malagoli" 4 | 5 | 6 | class MovieInfo: 7 | def __init__(self): 8 | self._title = "" 9 | self._original_title = "" 10 | self._year = "" 11 | self._director = "" 12 | self._duration = "" 13 | self._languages = [""] 14 | self._subtitle_languages = [""] 15 | self._part = "" 16 | self._score = "" 17 | 18 | def get_title(self): 19 | return self._title 20 | 21 | def get_original_title(self): 22 | """ 23 | return the original movie title, in the original language 24 | 25 | e.g.: the original movie title for Deep Red from Dario Argento, in italian 26 | language, is Profondo Rosso 27 | """ 28 | return self._original_title 29 | 30 | def get_year(self): 31 | return self._year 32 | 33 | def get_directors(self): 34 | return self._director 35 | 36 | def get_duration(self): 37 | return self._duration 38 | 39 | def get_language(self, index: int = 0): 40 | return self._languages[index] 41 | 42 | def get_subtitle_language(self, index: int = 0): 43 | return self._subtitle_languages[index] 44 | 45 | def get_part(self): 46 | return self._part 47 | 48 | def get_score(self): 49 | return self._score 50 | 51 | def fill_with_guessed_info(self, guessed_info: MovieGuessedInfo): 52 | self._title = guessed_info.get_title() 53 | self._original_title = self._title 54 | self._year = guessed_info.get_year() 55 | self._languages = guessed_info.get_language() 56 | self._subtitle_languages = guessed_info.get_subtitle_language() 57 | self._part = guessed_info.get_cd_number() 58 | -------------------------------------------------------------------------------- /src/movie_tmdb_info.py: -------------------------------------------------------------------------------- 1 | import tmdbsimple as tmdb 2 | 3 | __author__ = "Alberto Malagoli" 4 | 5 | 6 | class MovieTMDBInfo: 7 | # TODO delete this comment 8 | # { 9 | # 'total_pages': 1, 10 | # 'page': 1, 11 | # 'results': [ 12 | # {'genre_ids': [18, 10749], 13 | # 'overview': "At Princeton University, John Nash struggles to make a worthwhile contribution to serve as his legacy to the world of mathematics. He finally makes a revolutionary breakthrough that will eventually earn him the Nobel Prize. After graduate school he turns to teaching, becoming romantically involved with his student Alicia. Meanwhile the government asks his help with breaking Soviet codes, which soon gets him involved in a terrifying conspiracy plot. Nash grows more and more paranoid until a discovery that turns his entire world upside down. Now it is only with Alicia's help that he will be able to recover his mental strength and regain his status as the great mathematician we know him as today..", 14 | # 'backdrop_path': '/5YF6MwuuKBRKLUE2dz3wetkgxAE.jpg', 15 | # 'original_language': 'en', 16 | # 'adult': False, 17 | # 'release_date': '2001-12-12', 18 | # 'original_title': 'A Beautiful Mind', 19 | # 'video': False, 20 | # 'vote_average': 7.27, 21 | # 'popularity': 2.245206, 22 | # 'poster_path': '/4SFqHDZ1NvWdysucWbgnYlobdxC.jpg', 23 | # 'id': 453, 24 | # 'vote_count': 1024, 25 | # 'title': 'A Beautiful Mind'} 26 | # ], 27 | # 'total_results': 1} 28 | 29 | def __init__(self): 30 | self.__id = 0 31 | self.__title = "" 32 | self.__original_title = "" 33 | self.__year = "" 34 | self.__original_language = "" 35 | self.__poster_path = "" 36 | self.__director = "" 37 | self.__duration = "" 38 | 39 | def get_title(self) -> str: 40 | return self.__title 41 | 42 | def get_original_title(self) -> str: 43 | return self.__original_title 44 | 45 | def get_year(self) -> str: 46 | return self.__year 47 | 48 | def get_original_language(self) -> str: 49 | return self.__original_language 50 | 51 | def get_poster_path(self) -> str: 52 | return self.__poster_path 53 | 54 | def get_director(self) -> str: 55 | return self.__director 56 | 57 | def get_duration(self) -> str: 58 | return self.__duration 59 | 60 | def fill_with_search_result(self, result): 61 | self.__id = result["id"] 62 | self.__title = result["title"] 63 | self.__original_title = result["original_title"] 64 | # release date is like 2001-12-12 65 | self.__year = result["release_date"].split("-")[0] 66 | self.__original_language = result["original_language"] 67 | self.__poster_path = result["poster_path"] 68 | movie = tmdb.Movies(self.__id) 69 | movie_info = movie.info() 70 | self.__duration = str(movie_info["runtime"]) 71 | movie_credits = movie.credits() 72 | for person in movie_credits["crew"]: 73 | if person["job"] == "Director": 74 | if self.__director != "": 75 | self.__director += ", " 76 | self.__director += person["name"] 77 | print(self.__director) 78 | -------------------------------------------------------------------------------- /src/preferences.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import QSettings 2 | 3 | __author__ = "Alberto Malagoli" 4 | 5 | 6 | class Preferences: 7 | """ 8 | 9 | """ 10 | 11 | DURATION_REPRESENTATION_MINUTES = 0 12 | DURATION_REPRESENTATION_HOURS_MINUTES = 1 13 | 14 | __WORDS_SEPARATOR = "words_separator" 15 | __LANGUAGE_REPRESENTATION = "language_representation" 16 | __DURATION_REPRESENTATION = "duration_representation" 17 | __STATS_AGREEMENT = "stats_agreement" 18 | __RENAMING_RULE = "renaming_rule" 19 | __FIRST_TIME_OPENING = "first_time_opening" 20 | __LAST_VISITED_DIRECTORY = "last_visited_directory" 21 | 22 | def __init__(self): 23 | self.__preferences = QSettings("preferences.ini", QSettings.IniFormat) 24 | 25 | # 26 | # last_visited_directory 27 | # 28 | 29 | def get_last_visited_directory(self) -> str: 30 | return self.__preferences.value(Preferences.__LAST_VISITED_DIRECTORY, "") 31 | 32 | def set_last_visited_directory(self, value: str): 33 | self.__preferences.setValue(Preferences.__LAST_VISITED_DIRECTORY, value) 34 | 35 | # 36 | # first_time_opening 37 | # 38 | 39 | def get_first_time_opening(self) -> bool: 40 | return self.__preferences.value(Preferences.__FIRST_TIME_OPENING, True).toBool() 41 | 42 | def set_first_time_opening(self, value: bool): 43 | self.__preferences.setValue(Preferences.__FIRST_TIME_OPENING, value) 44 | 45 | # 46 | # renaming_rule 47 | # 48 | 49 | def get_renaming_rule(self) -> str: 50 | return self.__preferences.value(Preferences.__RENAMING_RULE, "") 51 | 52 | def set_renaming_rule(self, value: str): 53 | self.__preferences.setValue(Preferences.__RENAMING_RULE, value) 54 | 55 | # 56 | # stats_agreement 57 | # 58 | 59 | def get_stats_agreement(self) -> bool: 60 | return self.__preferences.value(Preferences.__STATS_AGREEMENT, False).toBool() 61 | 62 | def set_stats_agreement(self, value: bool): 63 | self.__preferences.setValue(Preferences.__STATS_AGREEMENT, value) 64 | 65 | # 66 | # duration_representation 67 | # 68 | 69 | def get_duration_representation(self) -> int: 70 | return self.__preferences.value(Preferences.__DURATION_REPRESENTATION, 0) 71 | 72 | def set_duration_representation(self, value: int): 73 | self.__preferences.setValue(Preferences.__DURATION_REPRESENTATION, value) 74 | 75 | # 76 | # language_representation 77 | # 78 | 79 | def get_language_representation(self) -> int: 80 | return self.__preferences.value(Preferences.__LANGUAGE_REPRESENTATION, 0).toInt() 81 | 82 | def set_language_representation(self, value: int): 83 | self.__preferences.setValue(Preferences.__LANGUAGE_REPRESENTATION, value) 84 | 85 | # 86 | # words_separator 87 | # 88 | 89 | def get_words_separator(self) -> int: 90 | return self.__preferences.value(Preferences.__WORDS_SEPARATOR, 0).toInt() 91 | 92 | def set_words_separator(self, value: int): 93 | self.__preferences.setValue(Preferences.__WORDS_SEPARATOR, value) 94 | 95 | 96 | preferences = Preferences() 97 | -------------------------------------------------------------------------------- /src/preferences_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QDialog, QApplication 2 | from PyQt5.uic import loadUi 3 | from preferences import preferences 4 | 5 | __author__ = "Alberto Malagoli" 6 | 7 | 8 | class PreferencesDialog(QDialog): 9 | STATS_AGREE = 1 10 | STATS_DISAGREE = 0 11 | 12 | DURATION_REPRESENTATIONS = ( 13 | "Minutes only", 14 | "Hours and minutes", 15 | ) 16 | LANGUAGE_REPRESENTATIONS = ( 17 | "English name", 18 | "3-letters", 19 | ) 20 | WORDS_SEPARATORS_REPRESENTATIONS = ( 21 | ", (comma-space)", 22 | "- (space-dash-space)", 23 | " (space)", 24 | ) 25 | 26 | WORDS_SEPARATORS = (', ', ' - ', ' ',) 27 | 28 | def __init__(self, parent): 29 | QDialog.__init__(self, parent) 30 | 31 | # load UI 32 | self.ui = loadUi("preferences_dialog.ui", self) 33 | # load settings 34 | # TODO 35 | # self.load_settings() 36 | ## slots connection 37 | self.ui.radio_agree.clicked.connect(self.stats_agreement_agree) 38 | self.ui.radio_disagree.clicked.connect(self.stats_agreement_disagree) 39 | self.ui.combo_duration.currentIndexChanged.connect(self.duration_representation_changed) 40 | self.ui.combo_language.currentIndexChanged.connect(self.language_representation_changed) 41 | self.ui.combo_words_separator.currentIndexChanged.connect(self.words_separator_representation_changed) 42 | 43 | self.ui.button_close.clicked.connect(self.close) 44 | 45 | self.ui.tab_widget.setCurrentIndex(0) 46 | 47 | def load_settings(self): 48 | """ 49 | loads settings, and sets GUI elements according to 50 | user choices 51 | """ 52 | 53 | # get usage statistics agreement choice 54 | stats_agreement = preferences.get_stats_agreement() 55 | # set radio buttons checked 56 | if stats_agreement == self.STATS_AGREE: 57 | self.ui.radio_agree.setChecked(True) 58 | else: 59 | self.ui.radio_disagree.setChecked(True) 60 | 61 | duration_representation = preferences.get_duration_representation() 62 | self.ui.combo_duration.setCurrentIndex(duration_representation) 63 | language_representation = preferences.get_language_representation() 64 | self.ui.combo_language.setCurrentIndex(language_representation) 65 | words_separator = preferences.get_words_separator() 66 | self.ui.combo_words_separator.setCurrentIndex(words_separator) 67 | 68 | def stats_agreement_agree(self, checked): 69 | """ 70 | called when user clicks on radio button to agree with 71 | usage statistics agreement 72 | """ 73 | 74 | # save value on settings file 75 | preferences.set_stats_agreement(True) 76 | 77 | def stats_agreement_disagree(self, checked): 78 | """ 79 | called when user clicks on radio button to disagree with 80 | usage statistics agreement 81 | """ 82 | 83 | # save value on settings file 84 | preferences.set_stats_agreement(False) 85 | 86 | def duration_representation_changed(self, index): 87 | preferences.set_duration_representation(index) 88 | 89 | def language_representation_changed(self, index): 90 | preferences.set_language_representation(index) 91 | 92 | def words_separator_representation_changed(self, index): 93 | preferences.set_words_separator(index) 94 | 95 | def close(self): 96 | # TODO 97 | # send_usage_statistics() 98 | self.accept() 99 | -------------------------------------------------------------------------------- /src/renaming_rule_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QDialog, QApplication 2 | from PyQt5.uic import loadUi 3 | from movie import Movie 4 | from preferences import preferences 5 | from preferences_dialog import PreferencesDialog 6 | 7 | __author__ = "Alberto Malagoli" 8 | 9 | 10 | class RenamingRuleDialog(QDialog): 11 | TITLE = "Title" 12 | ORIGINAL_TITLE = "Original title" 13 | YEAR = "Year" 14 | DIRECTOR = "Director" 15 | DURATION = "Duration" 16 | LANGUAGE = "Language" 17 | OPENED_ROUND_BRACKET = "(" 18 | CLOSED_ROUND_BRACKET = ")" 19 | OPENED_SQUARE_BRACKET = "[" 20 | CLOSED_SQUARE_BRACKET = "]" 21 | OPENED_CURLY_BRACKET = "{" 22 | CLOSED_CURLY_BRACKET = "}" 23 | 24 | RENAMING_TO_VISUAL_RULE = { 25 | Movie.TITLE: TITLE, 26 | Movie.ORIGINAL_TITLE: ORIGINAL_TITLE, 27 | Movie.YEAR: YEAR, 28 | Movie.DIRECTOR: DIRECTOR, 29 | Movie.DURATION: DURATION, 30 | Movie.LANGUAGE: LANGUAGE, 31 | OPENED_ROUND_BRACKET: OPENED_ROUND_BRACKET, 32 | CLOSED_ROUND_BRACKET: CLOSED_ROUND_BRACKET, 33 | OPENED_SQUARE_BRACKET: OPENED_SQUARE_BRACKET, 34 | CLOSED_SQUARE_BRACKET: CLOSED_SQUARE_BRACKET, 35 | OPENED_CURLY_BRACKET: OPENED_CURLY_BRACKET, 36 | CLOSED_CURLY_BRACKET: CLOSED_CURLY_BRACKET 37 | } 38 | 39 | VISUAL_TO_RENAMING_RULE = { 40 | TITLE: Movie.TITLE, 41 | ORIGINAL_TITLE: Movie.ORIGINAL_TITLE, 42 | YEAR: Movie.YEAR, 43 | DIRECTOR: Movie.DIRECTOR, 44 | DURATION: Movie.DURATION, 45 | LANGUAGE: Movie.LANGUAGE, 46 | OPENED_ROUND_BRACKET: OPENED_ROUND_BRACKET, 47 | CLOSED_ROUND_BRACKET: CLOSED_ROUND_BRACKET, 48 | OPENED_SQUARE_BRACKET: OPENED_SQUARE_BRACKET, 49 | CLOSED_SQUARE_BRACKET: CLOSED_SQUARE_BRACKET, 50 | OPENED_CURLY_BRACKET: OPENED_CURLY_BRACKET, 51 | CLOSED_CURLY_BRACKET: CLOSED_CURLY_BRACKET 52 | } 53 | 54 | def __init__(self, parent, preferences_dialog): 55 | QDialog.__init__(self, parent) 56 | 57 | self.ui = loadUi("renaming_rule_dialog.ui", self) 58 | 59 | self.ui.preferences_dialog = preferences_dialog 60 | 61 | # creates an example movie, used to test the renaming rule 62 | self.example_movie = Movie() 63 | 64 | # TODO 65 | # self.populate_list_visual_rule() 66 | # self.update_representations() 67 | # TODO 68 | # renaming_rule = utils.preferences.value("renaming_rule").toString() 69 | # self.update_example_movie(renaming_rule) 70 | 71 | ## slots connection 72 | self.ui.list_visual_rule.model().rowsInserted.connect(self.rule_changed) 73 | self.ui.list_visual_rule.model().rowsRemoved.connect(self.rule_changed) 74 | 75 | self.ui.button_remove_rule.clicked.connect(self.remove_rule) 76 | self.ui.button_clean_rule.clicked.connect(self.clean_rule) 77 | 78 | self.ui.button_add_title.clicked.connect(self.add_title) 79 | self.ui.button_add_original_title.clicked.connect(self.add_original_title) 80 | self.ui.button_add_year.clicked.connect(self.add_year) 81 | self.ui.button_add_director.clicked.connect(self.add_director) 82 | self.ui.button_add_duration.clicked.connect(self.add_duration) 83 | self.ui.button_add_language.clicked.connect(self.add_language) 84 | 85 | self.ui.button_add_round_brackets.clicked.connect(self.add_round_brackets) 86 | self.ui.button_add_square_brackets.clicked.connect(self.add_square_brackets) 87 | self.ui.button_add_curly_brackets.clicked.connect(self.add_curly_brackets) 88 | 89 | self.ui.button_show_preferences.clicked.connect(self.show_preferences) 90 | 91 | self.ui.button_close.clicked.connect(self.close) 92 | 93 | def populate_list_visual_rule(self): 94 | """ 95 | populate renaming rule by rule read from settings 96 | """ 97 | 98 | renaming_rule = preferences.get_renaming_rule() 99 | # split rule 100 | rules = renaming_rule.split('.') 101 | # if rule is empty, reset it to 'title' 102 | if rules[0] == '': 103 | rules[0] = Movie.TITLE 104 | renaming_rule = Movie.TITLE 105 | preferences.set_renaming_rule(renaming_rule) 106 | visual_rule = [] 107 | # loop on rules 108 | for rule in rules: 109 | visual_rule.append(self.RENAMING_TO_VISUAL_RULE[rule]) 110 | self.ui.list_visual_rule.addItems(visual_rule) 111 | 112 | def update_representations(self): 113 | duration_index = preferences.get_duration_representation() 114 | duration_representation = PreferencesDialog.DURATION_REPRESENTATIONS[duration_index] 115 | self.ui.label_duration_representation.setText(duration_representation) 116 | 117 | language_index = preferences.get_language_representation() 118 | language_representation = PreferencesDialog.LANGUAGE_REPRESENTATIONS[language_index] 119 | self.ui.label_language_representation.setText(language_representation) 120 | 121 | separator_index = preferences.get_words_separator() 122 | separator_representation = PreferencesDialog.WORDS_SEPARATORS_REPRESENTATIONS[separator_index] 123 | self.ui.label_separator_representation.setText(separator_representation) 124 | 125 | def update_example_movie(self, renaming_rule): 126 | # generate new name for example movie 127 | example_movie_new_name = self.example_movie.generate_new_name(renaming_rule) 128 | # show it on label 129 | self.ui.label_example_movie_name.setText(example_movie_new_name) 130 | 131 | def rule_changed(self, parent=None, start=None, end=None): 132 | """ 133 | called when renaming rule changes 134 | 135 | creates and saves new renaming rule, and generate the movie example's new name 136 | """ 137 | 138 | rule = [] 139 | for index in range(self.ui.list_visual_rule.count()): 140 | text = self.ui.list_visual_rule.item(index).text() 141 | # when an item is moved inside the list_visual_rule, firstly 142 | # a new empty item is inserted into the destination location, then 143 | # the item from source location is deleted. that function is called 144 | # for both events (insertion and deletion), and when is called for 145 | # the insertion event after a list items move, the list contains an empty item, 146 | # which is a kind of error. 147 | if text != '': 148 | rule.append(self.VISUAL_TO_RENAMING_RULE[text]) 149 | # creates renaming rule 150 | renaming_rule = '.'.join(rule) 151 | # save renaming rule on settings 152 | preferences.set_renaming_rule(renaming_rule) 153 | # update example movie 154 | self.update_example_movie(renaming_rule) 155 | 156 | def remove_rule(self): 157 | """ 158 | removes selected rule 159 | """ 160 | 161 | # get selected items in rule 162 | selected_items = self.ui.list_visual_rule.selectedItems() 163 | # remove its from list 164 | for item in reversed(selected_items): 165 | row = self.ui.list_visual_rule.row(item) 166 | self.ui.list_visual_rule.takeItem(row) 167 | 168 | def clean_rule(self): 169 | """ 170 | cleans rule (remove all renaming rules) 171 | """ 172 | 173 | self.ui.list_visual_rule.clear() 174 | # needs to call rule_changed because clear() doesn't 175 | # throw any signal 176 | self.rule_changed() 177 | 178 | def add_title(self): 179 | """ 180 | add title to rule 181 | """ 182 | 183 | self.ui.list_visual_rule.addItem(self.TITLE) 184 | 185 | def add_original_title(self): 186 | """ 187 | add aka to rule 188 | """ 189 | 190 | self.ui.list_visual_rule.addItem(self.ORIGINAL_TITLE) 191 | 192 | def add_year(self): 193 | """ 194 | add year to rule 195 | """ 196 | 197 | self.ui.list_visual_rule.addItem(self.YEAR) 198 | 199 | def add_director(self): 200 | """ 201 | add director to rule 202 | """ 203 | 204 | self.ui.list_visual_rule.addItem(self.DIRECTOR) 205 | 206 | def add_duration(self): 207 | """ 208 | add runtime to rule 209 | """ 210 | 211 | self.ui.list_visual_rule.addItem(self.DURATION) 212 | 213 | def add_language(self): 214 | """ 215 | add language to rule 216 | """ 217 | 218 | self.ui.list_visual_rule.addItem(self.LANGUAGE) 219 | 220 | def add_round_brackets(self): 221 | """ 222 | add opened and closed round brackets to rule 223 | """ 224 | 225 | self.ui.list_visual_rule.addItem(self.OPENED_ROUND_BRACKET) 226 | self.ui.list_visual_rule.addItem(self.CLOSED_ROUND_BRACKET) 227 | 228 | def add_square_brackets(self): 229 | """ 230 | add opened and closed square brackets to rule 231 | """ 232 | 233 | self.ui.list_visual_rule.addItem(self.OPENED_SQUARE_BRACKET) 234 | self.ui.list_visual_rule.addItem(self.CLOSED_SQUARE_BRACKET) 235 | 236 | def add_curly_brackets(self): 237 | """ 238 | add opened and closed curly brackets to rule 239 | """ 240 | 241 | self.ui.list_visual_rule.addItem(self.OPENED_CURLY_BRACKET) 242 | self.ui.list_visual_rule.addItem(self.CLOSED_CURLY_BRACKET) 243 | 244 | def show_preferences(self): 245 | self.ui.preferences_dialog.exec_() 246 | # TODO 247 | # renaming_rule = utils.preferences.value("renaming_rule").toString() 248 | # self.update_representations() 249 | # update example movie 250 | # self.update_example_movie(renaming_rule) 251 | 252 | def close(self): 253 | # TODO 254 | # send_usage_statistics() 255 | self.accept() 256 | -------------------------------------------------------------------------------- /src/stats_agreement_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QDialog 2 | from PyQt5.uic import loadUi 3 | 4 | from preferences import preferences 5 | 6 | __author__ = "Alberto Malagoli" 7 | 8 | 9 | class StatsAgreementDialog(QDialog): 10 | def __init__(self, parent): 11 | QDialog.__init__(self, parent) 12 | 13 | # load UI 14 | self.ui = loadUi("stats_agreement_dialog.ui", self) 15 | # slots connection 16 | self.ui.radio_agree.clicked.connect(self.stats_agreement_agree) 17 | self.ui.radio_disagree.clicked.connect(self.stats_agreement_disagree) 18 | 19 | self.ui.button_box.accepted.connect(self.close) 20 | 21 | def stats_agreement_agree(self, checked): 22 | """ 23 | called when user clicks on radio button to agree with 24 | usage statistics agreement 25 | """ 26 | 27 | # save value on settings file 28 | preferences.set_stats_agreement(True) 29 | 30 | def stats_agreement_disagree(self, checked): 31 | """ 32 | called when user clicks on radio button to disagree with 33 | usage statistics agreement 34 | """ 35 | 36 | # save value on settings file 37 | preferences.set_stats_agreement(False) 38 | 39 | 40 | def close(self): 41 | self.accept() 42 | -------------------------------------------------------------------------------- /src/ui/ALBusyIndicator.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.5 2 | 3 | Row { 4 | id: layout 5 | width: itemWidth * itemsCount + itemsSpacing * (itemsCount - 1) 6 | height: 27 7 | spacing: itemsSpacing 8 | 9 | property int itemWidth: 4 10 | property int itemsCount: 9 11 | property int itemsSpacing: 3 12 | 13 | Repeater { 14 | model: itemsCount 15 | 16 | Rectangle { 17 | id: item 18 | width: itemWidth 19 | height: layout.height 20 | transform: scaleTransform 21 | color: "darkGray" 22 | property int itemIndex: index 23 | property Scale scaleTransform: Scale { 24 | yScale: 0.2 25 | origin.y: layout.height / 2 26 | } 27 | 28 | SequentialAnimation { 29 | id: animation 30 | running: true 31 | 32 | property int duration: 300 33 | 34 | NumberAnimation { 35 | duration: animation.duration * item.itemIndex 36 | } 37 | SequentialAnimation { 38 | loops: Animation.Infinite 39 | 40 | NumberAnimation { 41 | target: scaleTransform 42 | property: "yScale" 43 | to: 1.0 44 | duration: 100 45 | } 46 | NumberAnimation { 47 | target: scaleTransform 48 | property: "yScale" 49 | to: 0.2 50 | easing.type: Easing.OutCubic 51 | duration: 700 52 | } 53 | NumberAnimation { 54 | duration: animation.duration * layout.itemsCount 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/ui/main_window.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.5 2 | import QtQuick.Controls 1.4 3 | import QtQuick.Layouts 1.1 4 | import QtQuick.Window 2.0 5 | 6 | ApplicationWindow { 7 | id: window 8 | visible: true 9 | width: 700 10 | height: 600 11 | title: "ALMoviesRenamer" 12 | 13 | signal addMoviesClicked() 14 | signal addMoviesInFolderClicked() 15 | signal addMoviesInFolderAndSubfoldersClicked() 16 | signal removeSelectedMoviesClicked() 17 | signal removeAllMoviesClicked() 18 | signal showRenamingRuleDialogClicked() 19 | signal renameMoviesClicked() 20 | 21 | signal moviesSelectionChanged() 22 | signal movieAlternativeTitleChanged(var index) 23 | signal searchMovieClicked() 24 | 25 | property alias loadingPanelVisible: loadingPanel.visible 26 | property alias loadingPanelMovieTitle: loadingPanelMovieTitle.text 27 | 28 | property alias moviesTableModel: moviesTable.model 29 | property alias moviesTableCurrentRow: moviesTable.currentRow 30 | property var moviesTableSelection: moviesTable.getSelectedIndices() 31 | 32 | function getMoviesTableSelection() 33 | { 34 | var indices = [] 35 | moviesTable.selection.forEach(function(index){ 36 | indices.push(index) 37 | }) 38 | return indices 39 | } 40 | 41 | property alias movieInfoPanelVisible: movieInfoPanel.visible 42 | 43 | property alias movieAlternativeTitlesModel: movieAlternativeTitles.model 44 | property alias movieAlternativeTitleIndex: movieAlternativeTitles.currentIndex 45 | 46 | property alias movieTitle: movieTitle.text 47 | property alias movieOriginalTitle: movieOriginalTitle.text 48 | property alias movieYear: movieYear.text 49 | property alias movieDirectors: movieDirectors.text 50 | property alias movieDuration: movieDuration.text 51 | property alias movieLanguage: movieLanguage.text 52 | 53 | property alias searchAlternativeMovieProgressBarVisible: searchAlternativeMovieProgressBar.running 54 | property alias searchAlternativeTitle: searchAlternativeTitleTextField.text 55 | property alias searchAlternativeYear: searchAlternativeYearTextField.text 56 | property alias searchAlternativeLanguage: searchAlternativeLanguageTextField.text 57 | 58 | property alias movieRenamedPanelVisible: movieRenamedPanel.visible 59 | 60 | property alias movieErrorPanelVisible: movieErrorPanel.visible 61 | property alias movieError: movieError.text 62 | 63 | menuBar: MenuBar { 64 | Menu { 65 | title: "Movies" 66 | MenuItem { 67 | text: "Add movies..." 68 | iconSource: "../icons/movie_add.png" 69 | onTriggered: addMoviesClicked() 70 | } 71 | MenuItem { 72 | text: "Add all movies in folder..." 73 | iconSource: "../icons/movies_from_folder.png" 74 | onTriggered: addMoviesInFolderClicked() 75 | } 76 | MenuItem { 77 | text: "Add all movies in folder (including subfolders)..." 78 | iconSource: "../icons/movies_from_folder.png" 79 | onTriggered: addMoviesInFolderAndSubfoldersClicked() 80 | } 81 | MenuSeparator {} 82 | MenuItem { 83 | text: "Remove selected movies from list" 84 | iconSource: "../icons/movie_remove.png" 85 | onTriggered: removeSelectedMoviesClicked() 86 | } 87 | MenuItem { 88 | text: "Remove all movies from list" 89 | iconSource: "../icons/movie_erase.png" 90 | onTriggered: removeAllMoviesClicked() 91 | } 92 | MenuSeparator {} 93 | MenuItem { 94 | text: "Change renaming rule..." 95 | iconSource: "../icons/tag.png" 96 | onTriggered: showRenamingRuleDialogClicked() 97 | } 98 | MenuSeparator {} 99 | MenuItem { 100 | text: "Rename movies" 101 | iconSource: "../icons/pencil.png" 102 | onTriggered: renameMoviesClicked() 103 | } 104 | } 105 | Menu { 106 | title: "Application" 107 | MenuItem { 108 | text: "Preferences..." 109 | } 110 | MenuItem { 111 | text: "About..." 112 | } 113 | } 114 | } 115 | 116 | ColumnLayout { 117 | anchors.fill: parent 118 | 119 | RowLayout { 120 | spacing: 6 121 | Layout.leftMargin: 11 122 | Layout.rightMargin: 11 123 | Layout.topMargin: 11 124 | Layout.bottomMargin: 11 125 | 126 | Button { 127 | text: "Add movies" 128 | iconSource: "../icons/movie_add.png" 129 | onClicked: addMoviesClicked() 130 | } 131 | Button { 132 | text: "Remove movies" 133 | iconSource: "../icons/movie_remove.png" 134 | onClicked: removeSelectedMoviesClicked() 135 | } 136 | Item { 137 | width: 11 138 | } 139 | Button { 140 | text: "Renaming rule" 141 | iconSource: "../icons/tag.png" 142 | onClicked: showRenamingRuleDialogClicked() 143 | } 144 | Item { 145 | width: 11 146 | } 147 | Button { 148 | text: "Rename movies" 149 | iconSource: "../icons/pencil.png" 150 | onClicked: renameMoviesClicked() 151 | } 152 | } 153 | 154 | Rectangle { 155 | Layout.fillWidth: true 156 | height: 1 157 | color: "lightGray" 158 | } 159 | 160 | ColumnLayout { 161 | id: loadingPanel 162 | spacing: 6 163 | Layout.leftMargin: 11 164 | Layout.rightMargin: 11 165 | Layout.topMargin: 11 166 | Layout.bottomMargin: 11 167 | 168 | Label { 169 | text: "Getting information from:" 170 | } 171 | Label { 172 | id: loadingPanelMovieTitle 173 | } 174 | BusyIndicator { 175 | running: loadingPanel.visible 176 | } 177 | Label { 178 | text: "This may take a while... I will play a sound when it finishes." 179 | } 180 | } 181 | 182 | TableView{ 183 | id: moviesTable 184 | 185 | Layout.leftMargin: 11 186 | Layout.rightMargin: 11 187 | Layout.topMargin: 11 188 | Layout.bottomMargin: 11 189 | Layout.fillWidth: true 190 | Layout.fillHeight: true 191 | 192 | model: [] 193 | 194 | selectionMode: SelectionMode.ContiguousSelection 195 | selection.onSelectionChanged: moviesSelectionChanged() 196 | 197 | function getSelectedIndices() 198 | { 199 | var indices = [] 200 | selection.forEach(function(index){ 201 | indices.push(index) 202 | }) 203 | return indices 204 | } 205 | 206 | onRowCountChanged: resizeColumnsToContents() 207 | 208 | TableViewColumn{ 209 | role: "original_name" 210 | title: "Original name" 211 | } 212 | TableViewColumn{ 213 | role: "new_name" 214 | title: "New name" 215 | } 216 | } 217 | 218 | ColumnLayout { 219 | id: movieInfoPanel 220 | spacing: 6 221 | Layout.leftMargin: 11 222 | Layout.rightMargin: 11 223 | Layout.topMargin: 11 224 | Layout.bottomMargin: 11 225 | 226 | Label { 227 | text: "Movie:" 228 | } 229 | ComboBox { 230 | id: movieAlternativeTitles 231 | Layout.fillWidth: true 232 | model: [] 233 | 234 | onCurrentIndexChanged: movieAlternativeTitleChanged(currentIndex) 235 | } 236 | GridLayout { 237 | rowSpacing: 6 238 | columnSpacing: 6 239 | columns: 2 240 | 241 | Label { text: "Title:" } 242 | Label { id: movieTitle } 243 | 244 | Label { text: "Original title:" } 245 | Label { id: movieOriginalTitle } 246 | 247 | Label { text: "Year:" } 248 | Label { id: movieYear } 249 | 250 | Label { text: "Director(s):" } 251 | Label { id: movieDirectors } 252 | 253 | Label { text: "Duration:" } 254 | Label { id: movieDuration } 255 | 256 | Label { text: "Language:" } 257 | Label { id: movieLanguage } 258 | } 259 | Rectangle { 260 | Layout.fillWidth: true 261 | height: 1 262 | color: "lightGray" 263 | } 264 | Label { 265 | text: "Not the right movie? Search for another one:" 266 | } 267 | GridLayout { 268 | rowSpacing: 6 269 | columnSpacing: 6 270 | columns: 3 271 | 272 | Label { text: "Title:" } 273 | Label { text: "Year:" } 274 | Label { text: "Language:" } 275 | TextField { 276 | id: searchAlternativeTitleTextField 277 | Layout.fillWidth: true 278 | placeholderText: "Title" 279 | } 280 | TextField { 281 | id: searchAlternativeYearTextField 282 | placeholderText: "Year" 283 | } 284 | TextField { 285 | id: searchAlternativeLanguageTextField 286 | placeholderText: "Language" 287 | } 288 | } 289 | RowLayout { 290 | spacing: 6 291 | Button { 292 | text: "Search" 293 | 294 | onClicked: searchMovieClicked() 295 | } 296 | BusyIndicator { 297 | id: searchAlternativeMovieProgressBar 298 | running: false 299 | } 300 | } 301 | } 302 | 303 | Label { 304 | id: movieRenamedPanel 305 | Layout.leftMargin: 11 306 | Layout.rightMargin: 11 307 | Layout.topMargin: 11 308 | Layout.bottomMargin: 11 309 | text: "This movie has been correctly renamed." 310 | color: "green" 311 | } 312 | 313 | ColumnLayout { 314 | id: movieErrorPanel 315 | spacing: 6 316 | Layout.leftMargin: 11 317 | Layout.rightMargin: 11 318 | Layout.topMargin: 11 319 | Layout.bottomMargin: 11 320 | 321 | Label { 322 | text: "There has been the following error during renaming:" 323 | } 324 | Label { 325 | id: movieError 326 | color: "red" 327 | } 328 | } 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/ui/main_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 503 10 | 841 11 | 12 | 13 | 14 | 15 | icons/brand.pngicons/brand.png 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 0 27 | 0 28 | 29 | 30 | 31 | TextLabel 32 | 33 | 34 | 35 | 36 | 37 | 38 | 0 39 | 40 | 41 | false 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 0 50 | 0 51 | 52 | 53 | 54 | <html><head/><body><p><span style=" font-weight:600;">This may take a while... I will play a sound when it finishes.</span></p></body></html> 55 | 56 | 57 | true 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 0 69 | 1 70 | 71 | 72 | 73 | Qt::ActionsContextMenu 74 | 75 | 76 | QAbstractItemView::NoEditTriggers 77 | 78 | 79 | QAbstractItemView::ExtendedSelection 80 | 81 | 82 | QAbstractItemView::SelectRows 83 | 84 | 85 | true 86 | 87 | 88 | false 89 | 90 | 91 | 92 | Original name 93 | 94 | 95 | 96 | 97 | New name 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 0 106 | 107 | 108 | 109 | 110 | 111 | 112 | Movie: 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | QFormLayout::AllNonFixedFieldsGrow 123 | 124 | 125 | 9 126 | 127 | 128 | 129 | 130 | 131 | true 132 | 133 | 134 | 135 | Title: 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 75 144 | true 145 | 146 | 147 | 148 | Un film molto figo 149 | 150 | 151 | Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | true 160 | 161 | 162 | 163 | Original title: 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 75 172 | true 173 | 174 | 175 | 176 | A really cool movie 177 | 178 | 179 | Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | true 188 | 189 | 190 | 191 | Year: 192 | 193 | 194 | 195 | 196 | 197 | 198 | 2012 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | true 207 | 208 | 209 | 210 | Director(s): 211 | 212 | 213 | 214 | 215 | 216 | 217 | A. Director 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | true 226 | 227 | 228 | 229 | Duration: 230 | 231 | 232 | 233 | 234 | 235 | 236 | 100' 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | true 245 | 246 | 247 | 248 | Language: 249 | 250 | 251 | 252 | 253 | 254 | 255 | Italian 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | Not the right movie? Search for another one: 265 | 266 | 267 | true 268 | 269 | 270 | 271 | 272 | 273 | Search for this title (add year for better results): 274 | 275 | 276 | 277 | 278 | 279 | 280 | 1 281 | 282 | 283 | 284 | 285 | 0 286 | 287 | 288 | 0 289 | 290 | 291 | 0 292 | 293 | 294 | 0 295 | 296 | 297 | 298 | 299 | 300 | DejaVu Sans Mono 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | Search... 309 | 310 | 311 | 312 | icons/magnifier.pngicons/magnifier.png 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 0 322 | 323 | 324 | 0 325 | 326 | 327 | 0 328 | 329 | 330 | 0 331 | 332 | 333 | 334 | 335 | Searching... 336 | 337 | 338 | Qt::AlignCenter 339 | 340 | 341 | 342 | 343 | 344 | 345 | 0 346 | 347 | 348 | false 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | <html><head/><body><p><span style=" font-size:11pt; color:#005000;">This movie has been correctly renamed.</span></p></body></html> 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | Qt::Vertical 378 | 379 | 380 | 381 | 0 382 | 0 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | There has been the following error during renaming: 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 75 399 | true 400 | 401 | 402 | 403 | <html><head/><body><p><span style="font-size:11pt; font-weight:400; color:#ff0000;">ERROR!</span></p></body></html> 404 | 405 | 406 | 407 | 408 | 409 | 410 | Qt::Vertical 411 | 412 | 413 | 414 | 0 415 | 0 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 0 430 | 0 431 | 503 432 | 22 433 | 434 | 435 | 436 | 437 | Movies 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | Program 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | false 463 | 464 | 465 | TopToolBarArea 466 | 467 | 468 | false 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | icons/movie_add.pngicons/movie_add.png 481 | 482 | 483 | Add movies... 484 | 485 | 486 | 487 | 488 | 489 | icons/movies_from_folder.pngicons/movies_from_folder.png 490 | 491 | 492 | Add all movies in folder... 493 | 494 | 495 | 496 | 497 | 498 | icons/movies_from_folder.pngicons/movies_from_folder.png 499 | 500 | 501 | Add all movies in folder (including subfolders)... 502 | 503 | 504 | 505 | 506 | 507 | icons/movie_remove.pngicons/movie_remove.png 508 | 509 | 510 | Remove selected movies from list 511 | 512 | 513 | Remove selected movies from list 514 | 515 | 516 | 517 | 518 | 519 | icons/movie_erase.pngicons/movie_erase.png 520 | 521 | 522 | Remove all movies from list 523 | 524 | 525 | Remove all movies from list 526 | 527 | 528 | 529 | 530 | 531 | icons/tag.pngicons/tag.png 532 | 533 | 534 | Change renaming rule 535 | 536 | 537 | Change renaming rule 538 | 539 | 540 | 541 | 542 | 543 | icons/pencil.pngicons/pencil.png 544 | 545 | 546 | Rename movies 547 | 548 | 549 | 550 | 551 | 552 | icons/wrench-screwdriver.pngicons/wrench-screwdriver.png 553 | 554 | 555 | Preferences... 556 | 557 | 558 | Change preferences 559 | 560 | 561 | 562 | 563 | 564 | icons/information.pngicons/information.png 565 | 566 | 567 | About... 568 | 569 | 570 | 571 | 572 | Copy title 573 | 574 | 575 | Ctrl+C 576 | 577 | 578 | 579 | 580 | Open containing folder... 581 | 582 | 583 | 584 | 585 | 586 | 587 | -------------------------------------------------------------------------------- /src/ui/main_window_view.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtGui import QWindow 2 | from PyQt5.QtQml import QQmlApplicationEngine 3 | 4 | from ui.movie_table_item import MovieTableItem 5 | 6 | LOADING_PANEL_VISIBLE_PROPERTY = "loadingPanelVisible" 7 | LOADING_PANEL_MOVIE_TITLE_PROPERTY = "loadingPanelMovieTitle" 8 | 9 | MOVIES_TABLE_MODEL_PROPERTY = "moviesTableModel" 10 | MOVIES_TABLE_CURRENT_ROW_PROPERTY = "moviesTableCurrentRow" 11 | MOVIES_TABLE_SELECTION_PROPERTY = "moviesTableSelection" 12 | 13 | MOVIE_INFO_PANEL_VISIBLE_PROPERTY = "movieInfoPanelVisible" 14 | 15 | MOVIE_ALTERNATIVE_TITLES_MODEL_PROPERTY = "movieAlternativeTitlesModel" 16 | MOVIE_ALTERNATIVE_TITLE_INDEX_PROPERTY = "movieAlternativeTitleIndex" 17 | 18 | MOVIE_TITLE_PROPERTY = "movieTitle" 19 | MOVIE_ORIGINAL_TITLE_PROPERTY = "movieOriginalTitle" 20 | MOVIE_YEAR_PROPERTY = "movieYear" 21 | MOVIE_DIRECTORS_PROPERTY = "movieDirectors" 22 | MOVIE_DURATION_PROPERTY = "movieDuration" 23 | MOVIE_LANGUAGE_PROPERTY = "movieLanguage" 24 | 25 | MOVIE_SEARCH_PROGRESS_BAR_VISIBLE_PROPERTY = "searchAlternativeMovieProgressBarVisible" 26 | MOVIE_SEARCH_ALTERNATIVE_TITLE_PROPERTY = "searchAlternativeTitle" 27 | MOVIE_SEARCH_ALTERNATIVE_YEAR_PROPERTY = "searchAlternativeYear" 28 | MOVIE_SEARCH_ALTERNATIVE_LANGUAGE_PROPERTY = "searchAlternativeLanguage" 29 | 30 | MOVIE_RENAMED_PANEL_VISIBLE_PROPERTY = "movieRenamedPanelVisible" 31 | 32 | MOVIE_ERROR_PANEL_VISIBLE_PROPERTY = "movieErrorPanelVisible" 33 | MOVIE_ERROR_PROPERTY = "movieError" 34 | 35 | 36 | class MainWindowView: 37 | def __init__(self): 38 | self.__movies_table_view_model = [] 39 | 40 | self.__engine = QQmlApplicationEngine() 41 | self.__engine.load("ui/main_window.qml") 42 | 43 | def __get_root_window(self) -> QWindow: 44 | return self.__engine.rootObjects()[0] 45 | 46 | def __get_property(self, property_name: str): 47 | return self.__get_root_window().property(property_name) 48 | 49 | def __set_property(self, property_name: str, property_value): 50 | return self.__get_root_window().setProperty(property_name, property_value) 51 | 52 | def get_movies_table_current_row(self) -> int: 53 | return self.__get_property(MOVIES_TABLE_CURRENT_ROW_PROPERTY) 54 | 55 | def get_movies_table_selection(self) -> [int]: 56 | # selection = self.__get_property(MOVIES_TABLE_SELECTION_PROPERTY) 57 | selection = self.__get_root_window().getMoviesTableSelection() 58 | # QJSValue to QVariant 59 | variant = selection.toVariant() 60 | # with a multiple selection, variant is a list of float 61 | indices = [] 62 | for i in variant: 63 | # float to int 64 | indices.append(int(i)) 65 | return indices 66 | 67 | def get_movie_search_alternative_title(self) -> str: 68 | return self.__get_property(MOVIE_SEARCH_ALTERNATIVE_TITLE_PROPERTY) 69 | 70 | def get_movie_search_alternative_year(self) -> str: 71 | return self.__get_property(MOVIE_SEARCH_ALTERNATIVE_YEAR_PROPERTY) 72 | 73 | def get_movie_search_alternative_language(self) -> str: 74 | return self.__get_property(MOVIE_SEARCH_ALTERNATIVE_LANGUAGE_PROPERTY) 75 | 76 | def set_loading_panel_movie_title(self, loading_info: str) -> None: 77 | self.__set_property(LOADING_PANEL_MOVIE_TITLE_PROPERTY, loading_info) 78 | 79 | def set_loading_panel_visible(self, visible: bool) -> None: 80 | self.__set_property(LOADING_PANEL_VISIBLE_PROPERTY, visible) 81 | 82 | def set_movie_info_panel_visible(self, visible: bool) -> None: 83 | self.__set_property(MOVIE_INFO_PANEL_VISIBLE_PROPERTY, visible) 84 | 85 | def set_movie_renamed_panel_visible(self, visible: bool) -> None: 86 | self.__set_property(MOVIE_RENAMED_PANEL_VISIBLE_PROPERTY, visible) 87 | 88 | def set_movie_error_panel_visible(self, visible: bool) -> None: 89 | self.__set_property(MOVIE_ERROR_PANEL_VISIBLE_PROPERTY, visible) 90 | 91 | def set_movie_search_progress_bar_visible(self, visible: bool) -> None: 92 | self.__set_property(MOVIE_SEARCH_PROGRESS_BAR_VISIBLE_PROPERTY, visible) 93 | 94 | def add_movie_table_item(self, original_name: str, new_name: str) -> None: 95 | movie_table_item = MovieTableItem(original_name, new_name) 96 | self.__movies_table_view_model.append(movie_table_item) 97 | # From Qt Documentation: 98 | # Note: There is no way for the view to know that the contents of a QList has changed. 99 | # If the QList changes, it is necessary to reset the model by calling QQmlContext::setContextProperty() again. 100 | self.__set_property(MOVIES_TABLE_MODEL_PROPERTY, self.__movies_table_view_model) 101 | 102 | def remove_movie_table_item(self, index: int) -> None: 103 | del self.__movies_table_view_model[index] 104 | self.__set_property(MOVIES_TABLE_MODEL_PROPERTY, self.__movies_table_view_model) 105 | 106 | def remove_all_movie_table_items(self) -> None: 107 | del self.__movies_table_view_model[:] 108 | self.__set_property(MOVIES_TABLE_MODEL_PROPERTY, self.__movies_table_view_model) 109 | 110 | def set_movie_alternative_titles_model(self, model: []) -> None: 111 | self.__set_property(MOVIE_ALTERNATIVE_TITLES_MODEL_PROPERTY, model) 112 | 113 | def set_movie_title(self, movie_title: str) -> None: 114 | self.__set_property(MOVIE_TITLE_PROPERTY, movie_title) 115 | 116 | def set_movie_original_title(self, movie_original_title: str) -> None: 117 | self.__set_property(MOVIE_ORIGINAL_TITLE_PROPERTY, movie_original_title) 118 | 119 | def set_movie_year(self, movie_year: str) -> None: 120 | self.__set_property(MOVIE_YEAR_PROPERTY, movie_year) 121 | 122 | def set_movie_directors(self, movie_directors) -> None: 123 | self.__set_property(MOVIE_DIRECTORS_PROPERTY, movie_directors) 124 | 125 | def set_movie_duration(self, movie_duration) -> None: 126 | self.__set_property(MOVIE_DURATION_PROPERTY, movie_duration) 127 | 128 | def set_movie_language(self, movie_language) -> None: 129 | self.__set_property(MOVIE_LANGUAGE_PROPERTY, movie_language) 130 | 131 | def set_movie_alternative_title_index(self, index: int) -> None: 132 | self.__set_property(MOVIE_ALTERNATIVE_TITLE_INDEX_PROPERTY, index) 133 | 134 | def set_movie_error(self, movie_error: str) -> None: 135 | self.__set_property(MOVIE_ERROR_PROPERTY, movie_error) 136 | 137 | def get_add_movies_clicked_signal(self): 138 | return self.__get_root_window().addMoviesClicked 139 | 140 | def get_add_movies_in_folder_clicked_signal(self): 141 | return self.__get_root_window().addMoviesInFolderClicked 142 | 143 | def get_add_movies_in_folder_and_subfolders_clicked_signal(self): 144 | return self.__get_root_window().addMoviesInFolderAndSubfoldersClicked 145 | 146 | def get_remove_selected_movies_clicked_signal(self): 147 | return self.__get_root_window().removeSelectedMoviesClicked 148 | 149 | def get_remove_all_movies_clicked_signal(self): 150 | return self.__get_root_window().removeAllMoviesClicked 151 | 152 | def get_show_renaming_rule_dialog_clicked_signal(self): 153 | return self.__get_root_window().showRenamingRuleDialogClicked 154 | 155 | def get_rename_movies_clicked_signal(self): 156 | return self.__get_root_window().renameMoviesClicked 157 | 158 | def get_movie_item_selected_signal(self): 159 | return self.__get_root_window().movieSelected 160 | 161 | def get_movie_alternative_title_changed_signal(self): 162 | return self.__get_root_window().movieAlternativeTitleChanged 163 | 164 | def get_search_movie_clicked_signal(self): 165 | return self.__get_root_window().searchMovieClicked 166 | 167 | def get_movies_selection_changed_signal(self): 168 | return self.__get_root_window().moviesSelectionChanged 169 | -------------------------------------------------------------------------------- /src/ui/movie_table_item.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import QObject, pyqtProperty 2 | 3 | 4 | class MovieTableItem(QObject): 5 | def __init__(self, original_name, new_name, parent=None): 6 | super().__init__(parent) 7 | self.__original_name = original_name 8 | self.__new_name = new_name 9 | 10 | @pyqtProperty('QString', constant=True) 11 | def original_name(self): 12 | return self.__original_name 13 | 14 | @original_name.setter 15 | def original_name(self, original_name): 16 | self.__original_name = original_name 17 | 18 | @pyqtProperty('QString', constant=True) 19 | def new_name(self): 20 | return self.__new_name 21 | 22 | @new_name.setter 23 | def new_name(self, new_name): 24 | self.__new_name = new_name -------------------------------------------------------------------------------- /src/ui/preferences_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 364 10 | 291 11 | 12 | 13 | 14 | Preferences 15 | 16 | 17 | 18 | icons/wrench-screwdriver.pngicons/wrench-screwdriver.png 19 | 20 | 21 | 22 | 23 | 24 | 1 25 | 26 | 27 | 28 | Renaming rule 29 | 30 | 31 | 32 | 33 | 34 | Representation of some attributes: 35 | 36 | 37 | 38 | 39 | 40 | 41 | QFormLayout::AllNonFixedFieldsGrow 42 | 43 | 44 | 45 | 46 | Duration: 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | Minutes only (e.g.: 100m) 55 | 56 | 57 | 58 | 59 | Hours and minutes (e.g.: 1h40m) 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Language: 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | English name (e.g.: English) 76 | 77 | 78 | 79 | 80 | 3-letters (e.g.: ENG) 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | Words separator: 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | , (comma-space) 97 | 98 | 99 | 100 | 101 | - (space-dash-space) 102 | 103 | 104 | 105 | 106 | (space) 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | Qt::Vertical 117 | 118 | 119 | 120 | 20 121 | 0 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | Usage statistics agreement 131 | 132 | 133 | 134 | 135 | 136 | 137 | 0 138 | 0 139 | 140 | 141 | 142 | <html><head/><body><p> 143 | In order to improve future releases of ALmoviesRenamer, we ask you to send 144 | <span style=" font-weight:600;"> 145 | anonimous usage statistics 146 | </span> 147 | of the program. 148 | </p><p> 149 | ALmoviesRenamer collects information about: 150 | </p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:5px; margin-bottom:5px; margin-left:5px; margin-right:5px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;"> 151 | chosen renaming rule; 152 | </span></li><li style=" margin-top:5px; margin-bottom:5px; margin-left:5px; margin-right:5px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;"> 153 | renaming rule attributes representation. 154 | </span></li></ul><p> 155 | These information are then sent to a server, in a complete anonymity, and nothing is used or memorized in order to know your identity. 156 | </p></body></html> 157 | 158 | 159 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 160 | 161 | 162 | true 163 | 164 | 165 | 166 | 167 | 168 | 169 | I agree, send anonymous usage statistics. 170 | 171 | 172 | true 173 | 174 | 175 | 176 | 177 | 178 | 179 | I don't agree. Do not send usage statistics. 180 | 181 | 182 | 183 | 184 | 185 | 186 | Qt::Vertical 187 | 188 | 189 | 190 | 20 191 | 0 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | Qt::Horizontal 206 | 207 | 208 | 209 | 40 210 | 20 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | Close 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /src/ui/renaming_rule_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 477 10 | 585 11 | 12 | 13 | 14 | Renaming rule 15 | 16 | 17 | 18 | icons/tag.pngicons/tag.png 19 | 20 | 21 | 22 | 23 | 24 | Define renaming rule using movie attributes: 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | Drag to reorder 34 | 35 | 36 | QAbstractItemView::NoEditTriggers 37 | 38 | 39 | Qt::MoveAction 40 | 41 | 42 | QAbstractItemView::ExtendedSelection 43 | 44 | 45 | QListView::Snap 46 | 47 | 48 | QListView::LeftToRight 49 | 50 | 51 | 3 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | icons/minus.pngicons/minus.png 60 | 61 | 62 | 63 | 24 64 | 24 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | Qt::Horizontal 73 | 74 | 75 | QSizePolicy::Fixed 76 | 77 | 78 | 79 | 20 80 | 20 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | icons/eraser.pngicons/eraser.png 93 | 94 | 95 | 96 | 24 97 | 24 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 0 111 | 0 112 | 113 | 114 | 115 | 116 | icons/plus.pngicons/plus.png 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | true 125 | 126 | 127 | 128 | 3-letters 129 | 130 | 131 | 132 | 133 | 134 | 135 | Director(s) 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 0 144 | 0 145 | 146 | 147 | 148 | 149 | icons/plus.pngicons/plus.png 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 0 158 | 0 159 | 160 | 161 | 162 | 163 | icons/plus.pngicons/plus.png 164 | 165 | 166 | 167 | 168 | 169 | 170 | Language 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 0 179 | 0 180 | 181 | 182 | 183 | 184 | icons/plus.pngicons/plus.png 185 | 186 | 187 | 188 | 189 | 190 | 191 | Duration 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | true 200 | 201 | 202 | 203 | 100m 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 0 212 | 0 213 | 214 | 215 | 216 | 217 | icons/plus.pngicons/plus.png 218 | 219 | 220 | 221 | 222 | 223 | 224 | Year 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 0 233 | 0 234 | 235 | 236 | 237 | 238 | icons/plus.pngicons/plus.png 239 | 240 | 241 | 242 | 243 | 244 | 245 | Original title 246 | 247 | 248 | 249 | 250 | 251 | 252 | Title 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | Qt::Horizontal 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | (...) 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 0 279 | 0 280 | 281 | 282 | 283 | 284 | icons/plus.pngicons/plus.png 285 | 286 | 287 | 288 | 289 | 290 | 291 | [...] 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 0 300 | 0 301 | 302 | 303 | 304 | 305 | icons/plus.pngicons/plus.png 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 0 314 | 0 315 | 316 | 317 | 318 | 319 | icons/plus.pngicons/plus.png 320 | 321 | 322 | 323 | 324 | 325 | 326 | {...} 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | Qt::Horizontal 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | Words are separated with: 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 0 353 | 0 354 | 355 | 356 | 357 | 358 | true 359 | 360 | 361 | 362 | , (comma) 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | Qt::Horizontal 372 | 373 | 374 | 375 | 376 | 377 | 378 | Change representation preferences... 379 | 380 | 381 | 382 | icons/wrench-screwdriver.pngicons/wrench-screwdriver.png 383 | 384 | 385 | 386 | 387 | 388 | 389 | Example 390 | 391 | 392 | 393 | 394 | 395 | 396 | true 397 | 398 | 399 | 400 | A.Really.Cool.Movie.2012.ENG.XviD-Republic.CD1 401 | 402 | 403 | 404 | 405 | 406 | 407 | will be renamed into: 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 75 416 | true 417 | 418 | 419 | 420 | A really cool movie (2012) 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | Qt::Horizontal 433 | 434 | 435 | 436 | 40 437 | 20 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | Close 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | -------------------------------------------------------------------------------- /src/ui/renaming_rule_window.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.5 2 | import QtQuick.Controls 1.4 3 | import QtQuick.Window 2.0 4 | import QtQuick.Layouts 1.1 5 | import QtQml.Models 2.1 6 | 7 | ApplicationWindow { 8 | width: 600 9 | height: 520 10 | visible: true 11 | 12 | title: "Renaming rule" 13 | 14 | signal ruleChanged() 15 | signal removeRuleClicked(var index) 16 | signal removeAllRulesClicked() 17 | signal addTitleClicked() 18 | signal addOriginalTitleClicked() 19 | signal addYearClicked() 20 | signal addDirectorsClicked() 21 | signal addDurationClicked() 22 | signal addLanguageClicked() 23 | signal addRoundBracketsClicked() 24 | signal addSquareBracketsClicked() 25 | signal addCurlyBracketsClicked() 26 | signal closeClicked() 27 | 28 | function addRule(rule) { 29 | rulesListModel.append({ rule: rule }) 30 | ruleChanged() 31 | } 32 | 33 | function removeRule(index) { 34 | rulesListModel.remove(index) 35 | ruleChanged() 36 | } 37 | 38 | function removeAllRules() { 39 | rulesListModel.clear() 40 | ruleChanged() 41 | } 42 | 43 | function getRules() { 44 | var rules = [] 45 | for (var i = 0; i < rulesListModel.count; i++) { 46 | var rule = rulesListModel.get(i).rule 47 | rules.push(rule) 48 | } 49 | return rules 50 | } 51 | 52 | ColumnLayout { 53 | anchors.fill: parent 54 | anchors.leftMargin: 11 55 | anchors.rightMargin: 11 56 | anchors.topMargin: 11 57 | anchors.bottomMargin: 11 58 | 59 | spacing: 6 60 | 61 | Label { 62 | text: "Define renaming rule using movie attributes:" 63 | } 64 | 65 | RowLayout { 66 | Rectangle { 67 | anchors.fill: rulesList 68 | color: "white" 69 | } 70 | 71 | ListView { 72 | id: rulesList 73 | 74 | Layout.fillWidth: true 75 | Layout.fillHeight: true 76 | 77 | orientation: ListView.Horizontal 78 | model: rulesListDelegateModel 79 | spacing: 6 80 | 81 | moveDisplaced: Transition { 82 | NumberAnimation { 83 | properties: "x,y" 84 | easing.type: Easing.Bezier 85 | easing.bezierCurve: [0.4,0.0, 0.2,1.0, 1.0,1.0] 86 | duration: 150 87 | } 88 | } 89 | 90 | ListModel { 91 | id: rulesListModel 92 | } 93 | 94 | Component { 95 | id: rulesListDelegate 96 | 97 | MouseArea { 98 | id: dragArea 99 | 100 | anchors { 101 | top: parent.top 102 | bottom: parent.bottom 103 | } 104 | width: ruleContentBackground.width 105 | 106 | property bool held: false 107 | 108 | drag.target: held ? ruleContentBackground : undefined 109 | drag.axis: Drag.XAxis 110 | 111 | onPressed: held = true 112 | onReleased: held = false 113 | 114 | Rectangle { 115 | id: ruleContentBackground 116 | 117 | anchors { 118 | horizontalCenter: parent.horizontalCenter 119 | verticalCenter: parent.verticalCenter 120 | } 121 | width: ruleContent.width + 18 122 | height: ruleContent.height + 12 123 | 124 | border.width: 1 125 | border.color: "lightGray" 126 | radius: 5 127 | color: dragArea.held ? "lightGray" : "white" 128 | Behavior on color { ColorAnimation { duration: 100 } } 129 | 130 | Drag.active: dragArea.held 131 | Drag.source: dragArea 132 | Drag.hotSpot.x: width / 2 133 | Drag.hotSpot.y: height / 2 134 | 135 | states: State { 136 | when: dragArea.held 137 | 138 | ParentChange { target: ruleContentBackground; parent: rulesList } 139 | AnchorChanges { 140 | target: ruleContentBackground 141 | anchors { horizontalCenter: undefined; verticalCenter: undefined } 142 | } 143 | } 144 | 145 | RowLayout { 146 | id: ruleContent 147 | 148 | anchors.centerIn: parent 149 | width: ruleLabel.implicitWidth + ruleRemoveButton.implicitWidth + spacing 150 | height: ruleRemoveButton.implicitHeight 151 | 152 | spacing: 12 153 | 154 | 155 | Label { 156 | id: ruleLabel 157 | 158 | text: rule 159 | font.pointSize: 18 160 | } 161 | Label { 162 | id: ruleRemoveButton 163 | 164 | text: "\u00D7" 165 | font.pointSize: 24 166 | color: darkRed 167 | 168 | property string darkRed: "#D84315" 169 | property string lightRed: "#FFAB91" 170 | 171 | MouseArea { 172 | anchors.fill: parent 173 | 174 | onPressed: { 175 | ruleRemoveButton.color = ruleRemoveButton.lightRed 176 | } 177 | onReleased: { 178 | ruleRemoveButton.color = ruleRemoveButton.darkRed 179 | } 180 | onCanceled: { 181 | ruleRemoveButton.color = ruleRemoveButton.darkRed 182 | } 183 | 184 | onClicked: removeRuleClicked(index) 185 | } 186 | } 187 | } 188 | } 189 | DropArea { 190 | anchors.fill: parent 191 | 192 | onEntered: { 193 | rulesListDelegateModel.items.move( 194 | drag.source.DelegateModel.itemsIndex, 195 | dragArea.DelegateModel.itemsIndex) 196 | rulesListModel.move( 197 | drag.source.DelegateModel.itemsIndex, 198 | dragArea.DelegateModel.itemsIndex, 199 | 1) 200 | ruleChanged() 201 | } 202 | } 203 | } 204 | } 205 | 206 | DelegateModel { 207 | id: rulesListDelegateModel 208 | 209 | model: rulesListModel 210 | delegate: rulesListDelegate 211 | } 212 | } 213 | 214 | Button { 215 | text: "Clear" 216 | onClicked: removeAllRulesClicked() 217 | } 218 | } 219 | 220 | GridLayout { 221 | columns: 2 222 | 223 | Button { 224 | text: "Title" 225 | Layout.row: 1 226 | onClicked: addTitleClicked() 227 | } 228 | 229 | Button { 230 | text: "Original title" 231 | Layout.row: 2 232 | onClicked: addOriginalTitleClicked() 233 | } 234 | 235 | Button { 236 | text: "Year" 237 | Layout.row: 3 238 | onClicked: addYearClicked() 239 | } 240 | 241 | Button { 242 | text: "Director(s)" 243 | Layout.row: 4 244 | onClicked: addDirectorsClicked() 245 | } 246 | 247 | Button { 248 | text: "Duration" 249 | Layout.row: 5 250 | onClicked: addDurationClicked() 251 | } 252 | 253 | ComboBox { 254 | Layout.row: 5 255 | Layout.column: 2 256 | } 257 | 258 | Button { 259 | text: "Language" 260 | Layout.row: 6 261 | onClicked: addLanguageClicked() 262 | } 263 | 264 | Button { 265 | text: "(...)" 266 | Layout.row: 7 267 | onClicked: addRoundBracketsClicked() 268 | } 269 | 270 | Button { 271 | text: "[...]" 272 | Layout.row: 8 273 | onClicked: addSquareBracketsClicked() 274 | } 275 | 276 | Button { 277 | text: "{...}" 278 | Layout.row: 9 279 | onClicked: addCurlyBracketsClicked() 280 | } 281 | 282 | Label { 283 | text: "Words are separated with:" 284 | Layout.row: 10 285 | } 286 | 287 | ComboBox { 288 | Layout.row: 10 289 | Layout.column: 2 290 | } 291 | } 292 | 293 | Label { 294 | text: "Example:" 295 | } 296 | 297 | Label { 298 | text: "" 299 | } 300 | 301 | Label { 302 | text: "will be renamed into:" 303 | } 304 | 305 | Label { 306 | text: "" 307 | } 308 | 309 | RowLayout { 310 | Item { 311 | Layout.fillWidth: true 312 | } 313 | Button { 314 | text: "Close" 315 | onClicked: closeClicked() 316 | } 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/ui/renaming_rule_window_controller.py: -------------------------------------------------------------------------------- 1 | from preferences import preferences 2 | from ui.renaming_rule_window_view import RenamingRuleWindowView 3 | 4 | 5 | class RenamingRuleWindowController: 6 | def __init__(self): 7 | self.__main_window = RenamingRuleWindowView() 8 | self.__main_window.get_rule_changed_signal().connect(self.__rule_changed) 9 | self.__main_window.get_remove_rule_clicked_signal().connect(self.__remove_rule) 10 | self.__main_window.get_remove_all_rules_clicked_signal().connect(self.__remove_all_rules) 11 | self.__main_window.get_add_title_clicked_signal().connect(self.__add_title) 12 | self.__main_window.get_add_original_title_clicked_signal().connect(self.__add_original_title) 13 | self.__main_window.get_add_year_clicked_signal().connect(self.__add_year) 14 | self.__main_window.get_add_directors_clicked_signal().connect(self.__add_directors) 15 | self.__main_window.get_add_duration_clicked_signal().connect(self.__add_duration) 16 | self.__main_window.get_add_language_clicked_signal().connect(self.__add_language) 17 | self.__main_window.get_add_round_brackets_clicked_signal().connect(self.__add_round_brackets) 18 | self.__main_window.get_add_square_brackets_clicked_signal().connect(self.__add_square_brackets) 19 | self.__main_window.get_add_curly_brackets_clicked_signal().connect(self.__add_curly_brackets) 20 | self.__main_window.get_close_clicked_signal().connect(self.__close) 21 | 22 | def __rule_changed(self): 23 | rules = self.__main_window.get_rules() 24 | print(rules) 25 | # renaming_rule = ".".join(rules) 26 | # preferences.set_renaming_rule(renaming_rule) 27 | 28 | def __remove_rule(self, index: int): 29 | self.__main_window.remove_rule(index) 30 | 31 | def __remove_all_rules(self): 32 | self.__main_window.remove_all_rules() 33 | 34 | def __add_title(self): 35 | self.__main_window.add_rule("Title") 36 | 37 | def __add_original_title(self): 38 | self.__main_window.add_rule("OriginalTitle") 39 | 40 | def __add_year(self): 41 | self.__main_window.add_rule("Year") 42 | 43 | def __add_directors(self): 44 | self.__main_window.add_rule("Director") 45 | 46 | def __add_duration(self): 47 | self.__main_window.add_rule("Duration") 48 | 49 | def __add_language(self): 50 | self.__main_window.add_rule("Language") 51 | 52 | def __add_round_brackets(self): 53 | self.__main_window.add_rule("(") 54 | self.__main_window.add_rule(")") 55 | 56 | def __add_square_brackets(self): 57 | self.__main_window.add_rule("[") 58 | self.__main_window.add_rule("]") 59 | 60 | def __add_curly_brackets(self): 61 | self.__main_window.add_rule("{") 62 | self.__main_window.add_rule("}") 63 | 64 | def __close(self): 65 | pass 66 | -------------------------------------------------------------------------------- /src/ui/renaming_rule_window_view.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtQml import QQmlApplicationEngine 2 | 3 | 4 | # 5 | # TODO: 6 | # - update example movie while changing rule 7 | # - close button 8 | # - integrate with main UI 9 | # - load rule from preferences when loading UI 10 | # - save rule in preferences when it changes 11 | # 12 | 13 | class RenamingRuleWindowView: 14 | def __init__(self): 15 | self.__engine = QQmlApplicationEngine() 16 | self.__engine.load("ui/renaming_rule_window.qml") 17 | 18 | def __get_root_window(self): 19 | return self.__engine.rootObjects()[0] 20 | 21 | def __get_property(self, property_name: str): 22 | return self.__get_root_window().property(property_name) 23 | 24 | def __set_property(self, property_name: str, property_value): 25 | return self.__get_root_window().setProperty(property_name, property_value) 26 | 27 | def add_rule(self, rule: str): 28 | self.__get_root_window().addRule(rule) 29 | 30 | def remove_rule(self, index: int): 31 | self.__get_root_window().removeRule(index) 32 | 33 | def remove_all_rules(self): 34 | self.__get_root_window().removeAllRules() 35 | 36 | def get_rules(self) -> list: 37 | return self.__get_root_window().getRules().toVariant() 38 | 39 | def get_rule_changed_signal(self): 40 | return self.__get_root_window().ruleChanged 41 | 42 | def get_remove_rule_clicked_signal(self): 43 | return self.__get_root_window().removeRuleClicked 44 | 45 | def get_remove_all_rules_clicked_signal(self): 46 | return self.__get_root_window().removeAllRulesClicked 47 | 48 | def get_add_title_clicked_signal(self): 49 | return self.__get_root_window().addTitleClicked 50 | 51 | def get_add_original_title_clicked_signal(self): 52 | return self.__get_root_window().addOriginalTitleClicked 53 | 54 | def get_add_year_clicked_signal(self): 55 | return self.__get_root_window().addYearClicked 56 | 57 | def get_add_directors_clicked_signal(self): 58 | return self.__get_root_window().addDirectorsClicked 59 | 60 | def get_add_duration_clicked_signal(self): 61 | return self.__get_root_window().addDurationClicked 62 | 63 | def get_add_language_clicked_signal(self): 64 | return self.__get_root_window().addLanguageClicked 65 | 66 | def get_add_round_brackets_clicked_signal(self): 67 | return self.__get_root_window().addRoundBracketsClicked 68 | 69 | def get_add_square_brackets_clicked_signal(self): 70 | return self.__get_root_window().addSquareBracketsClicked 71 | 72 | def get_add_curly_brackets_clicked_signal(self): 73 | return self.__get_root_window().addCurlyBracketsClicked 74 | 75 | def get_close_clicked_signal(self): 76 | return self.__get_root_window().closeClicked 77 | -------------------------------------------------------------------------------- /src/ui/rules_list_item.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import QObject, pyqtProperty 2 | 3 | 4 | class RulesListItem(QObject): 5 | def __init__(self, rule, parent=None): 6 | super().__init__(parent) 7 | self.__rule = rule 8 | 9 | @pyqtProperty('QString', constant=True) 10 | def rule(self): 11 | return self.__rule 12 | 13 | @rule.setter 14 | def rule(self, rule): 15 | self.__rule = rule 16 | -------------------------------------------------------------------------------- /src/ui/stats_agreement_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 398 10 | 272 11 | 12 | 13 | 14 | Usage statistics agreement 15 | 16 | 17 | 18 | icons/brand.pngicons/brand.png 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | icons/exclamation.png 28 | 29 | 30 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 31 | 32 | 33 | 9 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 0 44 | 0 45 | 46 | 47 | 48 | <html><head/><body><p> 49 | In order to improve future releases of ALmoviesRenamer, we ask you to send 50 | <span style=" font-weight:600;"> 51 | anonimous usage statistics 52 | </span> 53 | of the program. 54 | </p><p> 55 | ALmoviesRenamer collects information about: 56 | </p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:5px; margin-bottom:5px; margin-left:5px; margin-right:5px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;"> 57 | chosen renaming rule; 58 | </span></li><li style=" margin-top:5px; margin-bottom:5px; margin-left:5px; margin-right:5px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;"> 59 | renaming rule attributes representation. 60 | </span></li></ul><p> 61 | These information are then sent to a server, in a complete anonymity, and nothing is used or memorized in order to know your identity. 62 | </p></body></html> 63 | 64 | 65 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 66 | 67 | 68 | true 69 | 70 | 71 | 72 | 73 | 74 | 75 | I agree, send anonymous usage statistics. 76 | 77 | 78 | true 79 | 80 | 81 | button_group 82 | 83 | 84 | 85 | 86 | 87 | 88 | I don't agree. Do not send usage statistics. 89 | 90 | 91 | button_group 92 | 93 | 94 | 95 | 96 | 97 | 98 | <html><head/><body><p> 99 | You can always change your choice later, under 100 | <span style=" font-weight:600;"> 101 | Program &gt; Preferences 102 | </span> 103 | menu. 104 | </p></body></html> 105 | 106 | 107 | true 108 | 109 | 110 | 111 | 112 | 113 | 114 | Qt::Horizontal 115 | 116 | 117 | QDialogButtonBox::Ok 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | __author__ = "Alberto Malagoli" 2 | 3 | # TODO instead of reading from txt file, create a python file with the dictionary already filled? 4 | # TODO in any case, create a different class to handle the language conversions 5 | def load_languages(): 6 | """ 7 | creates 3 dictionaries, used to convert a language name, a 3-letters ISO 8 | representation of a language, and a country name, into a language 9 | (with the representation used in movie class) 10 | """ 11 | 12 | global name_to_language_ 13 | name_to_language_ = dict() 14 | global alpha3_to_language_ 15 | alpha3_to_language_ = dict() 16 | global country_to_language_ 17 | country_to_language_ = dict() 18 | with open('languages.txt', 'r') as f: 19 | for line in f: 20 | name, alpha3, countries = str(line).rstrip('\n').rstrip('\r').split('|') 21 | language = [name, alpha3.upper()] 22 | name_to_language_.update({name: language}) 23 | alpha3_to_language_.update({alpha3: language}) 24 | countries = countries.split(';') 25 | for country in countries: 26 | country_to_language_.update({country: language}) 27 | 28 | 29 | def alpha3_to_language(given_alpha3): 30 | """ 31 | given a 3-letters ISO representation of a language, returns 32 | corresponding language 33 | """ 34 | 35 | try: 36 | return alpha3_to_language_[given_alpha3.lower()] 37 | except KeyError: 38 | return None 39 | 40 | 41 | def name_to_language(given_name): 42 | """ 43 | given a language English name, returns 44 | corresponding language 45 | """ 46 | 47 | try: 48 | return name_to_language_[given_name] 49 | except KeyError: 50 | return None 51 | 52 | 53 | def country_to_language(given_country): 54 | """ 55 | given a country name, returns corresponding language 56 | """ 57 | 58 | try: 59 | return country_to_language_[given_country] 60 | except KeyError: 61 | return None 62 | 63 | 64 | # TODO 65 | def send_usage_statistics(): 66 | pass 67 | # """ 68 | # checks user choice about sending usage statistics and 69 | # sends usage statistics to a dedicated web service 70 | # """ 71 | # 72 | # # get user choice about sending usage statistics 73 | # send_usage_statistics = utils.preferences.value("stats_agreement").toInt()[0] 74 | # # if user chose to send usage statistics 75 | # if send_usage_statistics == PreferencesDialog.STATS_AGREE: 76 | # # start sending thread 77 | # threading.Thread(target = send_usage_statistics_run).start() 78 | 79 | 80 | # TODO 81 | def send_usage_statistics_run(): 82 | pass 83 | # """ 84 | # sends usage statistics to a dedicated web service 85 | # """ 86 | # 87 | # # get preferences 88 | # rule = utils.preferences.value("renaming_rule").toString() 89 | # duration = utils.preferences.value("duration_representation").toString() 90 | # language = utils.preferences.value("language_representation").toString() 91 | # separator = utils.preferences.value("words_separator").toString() 92 | # # web service url 93 | # url = "http://almoviesrenamer.appspot.com/stats" 94 | # # create data 95 | # values = { 96 | # 'rule' : rule, 97 | # 'duration' : duration, 98 | # 'language' : language, 99 | # 'separator' : separator 100 | # } 101 | # data = urllib.urlencode(values) 102 | # # POST send data to web service 103 | # f = urllib2.urlopen(url, data) 104 | 105 | # TODO 106 | 107 | 108 | def check_connection(self): 109 | pass 110 | 111 | 112 | # """ 113 | # checks if internet connection is up. 114 | # 115 | # if internet connection is down, notifies the user with a message. 116 | # """ 117 | # 118 | # try: 119 | # # try to open a web URL 120 | # f = urllib2.urlopen("http://www.google.com/") 121 | # except URLError: 122 | # # if an error occurs, notify the user with a message 123 | # msg_box = QMessageBox() 124 | # msg_box.setWindowTitle( "Internet connection down?") 125 | # msg_box.setText( """ 126 | #

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 | #

Download it.

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 | --------------------------------------------------------------------------------