├── .gitignore ├── .gitmodules ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── db_interactor.py ├── folder.py ├── main.py ├── media.py ├── requirements.txt ├── resources ├── file-extensions.txt └── iso 639 2.json ├── scanner.py ├── subtitles.py ├── ui ├── SubCrawl.ui ├── __init__.py ├── bindings.py └── gui.py └── ui_example.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | .idea/ 3 | __pycache__/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dependencies/PTN"] 2 | path = dependencies/PTN 3 | url = https://github.com/divijbindlish/parse-torrent-name 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at lukaabramovic2@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Welcome contributor! 2 | 3 | Thank you for checking out the project and deciding that it would be a good idea to contribute and fix it up. Because let's be honest, the project is a fixer-upper if there's ever been one. We appreciate the time you have set apart in learning more about SubCrawl. For further information check out our Wiki. Let's get down to business! :briefcase: 4 | 5 | These guidelines are important as to develop this project in an organized fashion and help guide newcomers on the right path. 6 | 7 | # SETTING UP 8 | 9 | Currently you can set up SubCrawl to run by forking the repository to your designated folder and running `main.py`. Since this project is currently in it's beginning this is not how it is imagined it would run. You can view the vision and plan for the project on the [Wiki](https://github.com/lukaabra/SubCrawl/wiki/Vision). :telescope: 10 | 11 | ### Requirements: 12 | 13 | - Python 3 - version 3.7.1 14 | - [PyQt5](https://pypi.org/project/PyQt5/) - version: 5.11.3 15 | 16 | Also an internet connection is needed to fetch data from OMDb and OpenSubtitles servers. This project uses a module [PTN (parse-torrent-name)](https://github.com/divijbindlish/parse-torrent-name) which is included as a submodule. :mega: 17 | 18 | # CODE OF CONDUCT 19 | 20 | You can check out our Code of Conduct [here](https://github.com/lukaabra/SubCrawl/blob/master/CODE_OF_CONDUCT.md). :page_with_curl: 21 | 22 | # DISCLAIMER 23 | 24 | :exclamation: 25 | 26 | As mentioned in the README file, this project serves primarily as a platform to learn programming, Python, and developing in cooperation with the community. Things may not be done professionally at first, but we are here to learn and teach. :books: Any suggestions on improvement are more than welcome if they are properly and politely articulated. :speech_balloon: 27 | 28 | # CONTRIBUTING 29 | 30 | It a good practice to comment on an issue you want to take over so it is assigned to you. The reason for this is that it is possible you [fork](https://guides.github.com/activities/forking/) the repository and make a lot of changes to the code, only to find out later on that your [pull request](https://help.github.com/en/articles/creating-a-pull-request) has been rejected for God knows what reasons. To prevent this, reach out and ask to be assigned to an issue. :raising_hand: You can also open a new issue you found and ask to be assigned to it. 31 | There is no need to ask for assignment on issues which include: 32 | 33 | - Spelling / grammar fixes 34 | - Typo correction, white space and formatting changes 35 | - Comment clean up 36 | 37 | If the maintainers of the repository do not answer immediately, please have patience. They will contact you as soon as possible. :pray: 38 | 39 | ### Commits 40 | 41 | Commit messages should contain useful information regarding the changes made. Everyone was guilty at one point of committing `fixed crash` or `removed bug`. Strive for concise and clear documentation. Check out [this](https://medium.com/@andrewhowdencom/anatomy-of-a-good-commit-message-acd9c4490437) guide for more information. :pencil2: 42 | 43 | # FIRST TIMERS 44 | 45 | Under the [issues] tab you can find a whole lot of open problems with the project. I am absolutely sure you are capable of solving at least one problem from the list. Be sure to check those marked with **good first issue**. :school_satchel: If it does not look like it now, just fork the repository and go through the code. Documentation is still lacking at places but the general flow of the program is relatively understandable. I am sure even if you are not able to solve any kind of issue, you will find another one. 46 | 47 | # LABELING ISSUES 48 | 49 | Everyone is welcome to open new issues as soon as they find one. This includes the code, and also the documentation files. Follow the label convention: 50 | 51 | - **good first issue** People who are not familiar with the language and the code base are able to contribute and solve after a short amount of time studying the issue 52 | - **enhancement** Enrich the project with new features which currently do not exist 53 | - **question** Communicate with the community and agree to a most favourable solution 54 | - **invalid** This should not be like it is and should be fixed/extended regardless of whether it is working or not 55 | - **bug** This is not working and should be fixed 56 | - **help wanted** Ask the community for help, be it someone else taking over, asking for resources to solve the issue or pointers on how to approach the problem, or simply you don't have the time to tackle this right now 57 | 58 | # REPORTING BUGS 59 | 60 | When reporting a bug :bug: please follow the recommended template: 61 | 62 | 1. What version of Python are you using (python --version)? 63 | 2. What operating system are you using? 64 | 3. What did you do? 65 | 4. What did you expect to see? 66 | 5. What did you see instead? 67 | 68 | # TESTS 69 | 70 | Currently there no tests for this project. It is an [open issue](https://github.com/lukaabra/SubCrawl/issues/7) which we hope will be addressed soon. 71 | 72 | # DOCUMENTATION 73 | 74 | ### Code 75 | 76 | This project is using the [PEP8](https://www.python.org/dev/peps/pep-0008/) style guide and all the contributors are urged to follow it. 77 | All classes should be documented with the general usage of the class. Not to many detailed information regarding the usages of each method. Abstract the usage and tell the story of how this class is used. For example, instead of: 78 | 79 | ``` 80 | """ 81 | This class calls _download_from_opensubtitles_ which then logs in using _login_opensubtitles_ and queries the... 82 | """ 83 | ``` 84 | 85 | try something like: 86 | 87 | ``` 88 | """ 89 | This class logs the user to the Opensubtitles servers and queries them... 90 | """ 91 | ``` 92 | 93 | Also, be sure to indicate all the parameters and return values of each method, their general short description and type: 94 | 95 | ``` 96 | """ 97 | ... 98 | 99 | :param payload_for_sub_search: (dictionary) contains the information about the movie for which 100 | the subtitle will download 101 | :param proxy: ServerProxy.LogIn(username, password, language, useragent) 102 | 103 | :return download_data: (dictionary) contains crucial information for file downloading 104 | """ 105 | ``` 106 | 107 | But most of all, strive for code which is self-explanatory. 108 | 109 | ### Repository 110 | 111 | Create a pull request and describe what have you changed in the documentation. Please be sure to check out our [Wiki](https://github.com/lukaabra/SubCrawl/wiki/Vision) about the vision of the project, as to avoid conflicts in information between documentation files. 112 | Any changes to the "Vision" of the final look and feel of the project should first be opened in an issue and discussed between the community. If the support is great enough, the changes will be approved. 113 | 114 | ## CONTACT 115 | 116 | For any doubts or any extra information contact me at: 117 | 118 | :email: lukaabramovic2@gmail.com 119 | 120 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Luka Abramovic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **SubCrawl** 2 | 3 | ##### Scan for movies without subtitles and download them! 4 | 5 | ![Issues open](https://img.shields.io/github/issues-raw/lukaabra/SubCrawl.svg) 6 | 7 | ### What? 8 | 9 | The application enables the user to scan a designated directory for movies. :tv: 10 | The application can recognize what file is a movie and can recognize which movie has subtitles. :movie_camera: 11 | After the scanning part is completed, the user can choose individual movies for which to download the 12 | subtitles in the selected language, or can simply select all the movies. :japan: 13 | 14 | 15 | ### Why? 16 | 17 | This is a project I started as a way to learn Python 3 and to get familiar with the process of creating an application. I chose this theme because I could find use out of it (everyone needs subtitles from time to time). I know this type of application probably already exists and that is why I stated already that the primary reason for making this is to learn and grow as a developer. :mortar_board: 18 | 19 | ### Usage 20 | 21 | Clone the repository and install all of the dependencies using: 22 | 23 | `python3 -m pip install -r requirements.txt` 24 | 25 | Then run `main.py`: 26 | 27 | `python3 main.py` 28 | 29 | The interface (example below) will be opened and you can start using the app! 30 | 31 | ![GUI example](ui_example.png "GUI example") 32 | 33 | ### How? 34 | 35 | The whole application is built in Python 3. The GUI is built with PyQt5 and is designed in QtDesigner. 36 | File names are parsed using a package called [PTN (parse torrent name)](https://github.com/divijbindlish/parse-torrent-name) :mega:. 37 | 38 | Currently, the source of downloading subtitle data is OpenSubtitles. 39 | 40 | During the scanning of a selected directory, each recognized media file is compared with the OMDb database to figure out if the file is a movie. Afterwards, if all the movies have been recognized, the user can select which ones they want to download subtitles for. When the downloading is initiated the screen is freezed (see issues) and the program logs into the OpenSubtitles database. After the successful log in, the program pulls data about the subtitle files and writes those files in their respective movie directories. 41 | 42 | Each Python file holds classes which are grouped together thematically: 43 | 44 | 45 | :star: **main.py** initializes and runs the GUI 46 | 47 | :iphone: **gui.py** class Ui_Subcrawl which contains all the code to generate the GUI 48 | 49 | :fax: **scanner.py** class Scanner who's task is to scan a designated folder for files and directories 50 | 51 | :file_folder: **folder.py** classes Folder and File which help organize the structure of the traversal of files and directories in scanner.py 52 | 53 | :clapper: **media.py** classes Media and Movie which are in charge of verifying that the media file is a movie and organizing its data 54 | 55 | :page_facing_up: **subtitles.py** class SubtitlePreference which saves the language and source preference that the user chooses and SubtitleDownloader which does the heavy lifting :muscle: 56 | 57 | :floppy_disk: **db_interactor.py** class _DBInteractor which is in charge of database interaction, be it storing or retrieving entries 58 | 59 | :minidisc: **bindings.py** binds all the buttons in the gui.py with its logic 60 | 61 | ### Want to help? 62 | 63 | If you are willing to get your hands dirty and learn as you work, do not hesitate. Contribute with anything you think will improve the application. Beginners and masters of the craft are welcomed to join! :muscle: 64 | 65 | Check the open issues and start there. If you can't find anything of interest, let us know and we will find something! :question: 66 | We recommend checking out our [contributing guidelines](https://github.com/lukaabra/SubCrawl/blob/master/CONTRIBUTING.md) first before starting. 67 | 68 | ### Note 69 | 70 | This project is not yet at a point where it can be used as intended. There are still some issues that need to be resolved before it can be used properly. 71 | -------------------------------------------------------------------------------- /db_interactor.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import os 3 | 4 | from media import Media 5 | 6 | 7 | class _DBInteractor(object): 8 | 9 | def __init__(self, program_dir: str, rom_mode=False): 10 | """ 11 | Connects to a database and creates a cursor. Creates table all_movies if it does not exist. 12 | 13 | The all_movies table is for all of the media encountered in the scanned folder. 14 | The selected_movies is only for media which has been selected for subtitle downloading 15 | 16 | :param program_dir: (string) Specifies the directory in which the program is installed 17 | :param rom_mode: (Boolean) Specifies if the database will be open in Read Only Mode or not 18 | """ 19 | self._program_dir = program_dir 20 | os.chdir(self._program_dir) 21 | self.db_name = "media.db" 22 | self.db = None 23 | self.cursor = None 24 | self.duplicate_files = 0 25 | 26 | if rom_mode: 27 | current_path = os.getcwd() 28 | try: 29 | # Opens the file in read only mode 30 | self.db = sqlite3.connect("file:{}\media.db?mode=ro".format(current_path), uri=True) 31 | # Database does not exist 32 | except sqlite3.OperationalError: 33 | self._establish_connection() 34 | else: 35 | self._establish_connection() 36 | 37 | self._create_tables() 38 | 39 | def add_media_to_db(self, media: Media, table="all_movies"): 40 | """ 41 | Adds the file name, path, extension of the file, title, if there are any subtitles (bool in Python and 42 | int in SQLite3) and subtitle language string to the database. 43 | 44 | :param media: (Media) Media object of the file to add to the database or 45 | :param table: (string) table to which to add - "all_movies" or "selected_movies" 46 | """ 47 | if self._check_duplicate_media(media, table) and media.id: 48 | update_sql = "INSERT INTO {}(id, file_name, path, extension, title, year, rating, subtitles, sub_language)\ 49 | VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)".format(table) 50 | self.cursor.execute(update_sql, (media.id, media.file_name, media.path, media.extension, 51 | media.title, media.year, media.imdb_rating, 52 | str(media.subtitles), " ".join(media.sub_language))) 53 | 54 | def add_subtitle_search_data_to_db(self, subtitle_download_payload): 55 | update_sql = "INSERT INTO search_subs(subs_id, movie_id, file_name, path) VALUES(?, ?, ?, ?)" 56 | try: 57 | self.cursor.execute(update_sql, (int(subtitle_download_payload["IDSubtitleFile"]), 58 | subtitle_download_payload["imdbid"], 59 | subtitle_download_payload["file name"], 60 | subtitle_download_payload["movie directory"])) 61 | except sqlite3.IntegrityError: 62 | pass 63 | 64 | def add_subtitle_download_data_to_db(self, download_data: tuple): 65 | update_sql = "INSERT INTO download_subs(subs_id, bytecode) VALUES(?, ?)" 66 | sub_id, byte_data = download_data 67 | # byte_data is a regular string here!! 68 | # The conversion to byte data happens at the moment it is being written to a file 69 | try: 70 | self.cursor.execute(update_sql, (int(sub_id), byte_data)) 71 | except sqlite3.IntegrityError: 72 | print("Integrity error") 73 | pass 74 | 75 | def _check_duplicate_media(self, media: Media, table="all_movies") -> tuple or None: 76 | """ 77 | Checks for any duplicates in the database by first checking the file path and then the IMDb movie ID. 78 | 79 | :param media: (Media) Media object to check 80 | :param table: (string) name of the table in the database 81 | 82 | :return: True if there is no duplicate and None if a duplicate exists 83 | """ 84 | # Checks if there is already an entry with this specific info 85 | look_up_string = "SELECT * FROM {} WHERE {}=?" 86 | look_up = look_up_string.format(table, "path") 87 | self.cursor.execute(look_up, (media.path, )) 88 | first_duplicate_check = self.cursor.fetchone() 89 | 90 | if first_duplicate_check is None: 91 | look_up = look_up_string.format(table, "id") 92 | self.cursor.execute(look_up, (media.id, )) 93 | second_duplicate_check = self.cursor.fetchone() 94 | if second_duplicate_check is None: 95 | return True 96 | else: 97 | self.duplicate_files += 1 98 | return None 99 | else: 100 | self.duplicate_files += 1 101 | return None 102 | 103 | def check_if_entries_exist(self, table="all_movies"): 104 | """ 105 | Runs whenever the app is started. Checks if the entries in the database still exist. 106 | """ 107 | for entry in self.retrieve(table): 108 | file_path = entry[2] 109 | if not os.path.isfile(file_path): 110 | condition = ("path", file_path) 111 | self.delete_entry(condition) 112 | 113 | def clear_db(self, table="all_movies"): 114 | """ 115 | Clears the whole database of any data and entries inside. 116 | """ 117 | clear_command = "DROP TABLE IF EXISTS {}".format(table) 118 | self.cursor.execute(clear_command) 119 | self._create_tables() 120 | 121 | def _close_and_commit_db(self): 122 | """ 123 | Method that commits the changes done to the database and closes it up. 124 | """ 125 | self.cursor.connection.commit() 126 | self.cursor.close() 127 | self.db.close() 128 | 129 | def _create_tables(self): 130 | """ 131 | Creates tables "all_movies", "selected_movies", "search_subs", and "download_subs" if they do not exist. 132 | """ 133 | all_movies_table = "CREATE TABLE IF NOT EXISTS all_movies(id INTEGER PRIMARY KEY NOT NULL, " \ 134 | "file_name TEXT NOT NULL, path TEXT NOT NULL, extension TEXT, title TEXT NOT NULL, " \ 135 | "year TEXT, rating TEXT, subtitles TEXT NOT NULL, sub_language TEXT)" 136 | selected_movies_table = "CREATE TABLE IF NOT EXISTS selected_movies(id INTEGER PRIMARY KEY NOT NULL, " \ 137 | "file_name TEXT, path TEXT, extension TEXT, title TEXT, " \ 138 | "year TEXT, rating TEXT, subtitles TEXT, sub_language TEXT)" 139 | search_subs_table = "CREATE TABLE IF NOT EXISTS search_subs(subs_id INTEGER PRIMARY " \ 140 | "KEY NOT NULL, movie_id INTEGER NOT NULL, file_name TEXT, path TEXT)" 141 | download_subs_table = "CREATE TABLE IF NOT EXISTS download_subs(subs_id INTEGER PRIMARY KEY NOT NULL, " \ 142 | "bytecode TEXT NOT NULL)" 143 | self.cursor.execute(all_movies_table) 144 | self.cursor.execute(selected_movies_table) 145 | self.cursor.execute(search_subs_table) 146 | self.cursor.execute(download_subs_table) 147 | 148 | def commit_and_renew_cursor(self): 149 | """ 150 | Commits changes to the database, closes it and renews the cursor. 151 | """ 152 | self._close_and_commit_db() 153 | self._establish_connection() 154 | 155 | def copy_to_table(self, table_from, table_to, condition): 156 | """ 157 | Copies entries from "table_from" to "table_to" that satisfy the field condition. 158 | 159 | :param table_from: (String) name of table from which to copy values 160 | :param table_to: (string) name of table to which to copy values 161 | :param condition: (tuple) Tuple with two entries which specify the search condition. Example: 162 | ("id", "12345") The first element is the column and the second is the value. 163 | """ 164 | sql_command = "INSERT INTO {} SELECT * FROM {}".format(table_to, table_from) 165 | conditional = " WHERE {}=?".format(condition[0]) 166 | try: 167 | self.cursor.execute(sql_command + conditional, (condition[1], )) 168 | except sqlite3.IntegrityError: 169 | pass 170 | 171 | def delete_entry(self, condition, table="all_movies"): 172 | """ 173 | Deletes an entry from the database that matches the passed condition. 174 | 175 | :param table: (string) A string specifying from which table to retrieve information 176 | :param condition: (tuple) Tuple with two entries which specify the search condition. Example: 177 | ("id", "12345") The first element is the column and the second is the value. 178 | """ 179 | if_statement = " WHERE {}=?".format(condition[0]) 180 | sql_command = "DELETE FROM {}".format(table) 181 | self.cursor.execute(sql_command + if_statement, (condition[1], )) 182 | 183 | def _establish_connection(self): 184 | """ 185 | Connects to the database and renews the cursor. 186 | """ 187 | self.db = sqlite3.connect(self.db_name) 188 | self.cursor = self.db.cursor() 189 | 190 | def retrieve(self, table="all_movies", condition=None): 191 | """ 192 | Retrieves entries from the database. It would be desirable if the database is only being opened for retrieving 193 | to connect to in in ROM mode(__init__). 194 | 195 | :param table: (string) A string specifying from which table to retrieve information 196 | :param condition: (tuple) Tuple with two entries which specify the search condition. Example: 197 | ("id", "12345") The first element is the column and the second is the value. 198 | """ 199 | self._establish_connection() 200 | if condition: 201 | if_statement = " WHERE {}=?".format(condition[0]) 202 | result = self.cursor.execute("SELECT * FROM {}".format(table) + if_statement, (condition[1], )) 203 | search_result = tuple(entry for entry in result) 204 | return search_result 205 | else: 206 | self.cursor.execute("SELECT * FROM {}".format(table)) 207 | return self.cursor.fetchall() 208 | -------------------------------------------------------------------------------- /folder.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class Folder(object): 5 | """ 6 | Object which represents a folder in a PC. It has parents which should also be of type Folder and children which 7 | are of type File and stored in a list. When the children are locked, they are converted to a tuple and prevented 8 | from further changes. 9 | """ 10 | 11 | def __init__(self, path: str): 12 | """ 13 | Initiates data attributes to their default values and adds the name of the folder using regular expression. 14 | 15 | :param path: (string) absolute path of the folder 16 | """ 17 | self.path = path 18 | _, self.folder_name = os.path.split(path) 19 | self.parent = None 20 | self.children = [] 21 | 22 | def add_parent(self, parent): 23 | """ 24 | Adds a parent to this instance of the Folder object. 25 | 26 | :param parent: (Folder) parent of this instance of a Folder object 27 | """ 28 | self.parent = parent 29 | 30 | def add_child(self, child): 31 | """ 32 | Method used to add a child to this Folder. The child will be of type File which is the reason why this instance 33 | of the folder is added to it as a parent. 34 | Use only Folder methods for adding children and parents. 35 | 36 | :param child: (File) child inside of this instance of the folder 37 | """ 38 | self.children.append(child) 39 | child.add_parent(self) 40 | 41 | def lock_children(self): 42 | """ 43 | Turns the 'children' data attribute which is type list into a tuple and essentially prevents it from any 44 | further changes. 45 | """ 46 | self.children = tuple(self.children) 47 | 48 | def __str__(self): 49 | return "Folder name:\t{0.folder_name}\nFolder path:\t{0.path}\n".format(self) 50 | 51 | 52 | class File(object): 53 | """ 54 | Object which represents a file in a PC. Has parents as well as extensions. 55 | """ 56 | 57 | def __init__(self, path: str): 58 | """ 59 | Converts the absolute path to a file name and the extension of the file using regular expressions. 60 | 61 | :param path: (string) absolute path to the file 62 | """ 63 | self.path = path 64 | _, self.file_name = os.path.split(path) 65 | self.parent = None 66 | self.is_media = False 67 | self.is_sub = False 68 | self.file_name, self.extension = os.path.splitext(self.file_name) 69 | 70 | def add_parent(self, parent: Folder): 71 | """ 72 | Adds the parent to the data attribute of this instance of the File object. 73 | 74 | :param parent: (Folder) parent in which this file is located 75 | """ 76 | self.parent = parent 77 | 78 | def detect_media_or_sub(self, movie_extensions): 79 | """ 80 | Detects if the file is a media type file or a subtitle file. 81 | 82 | :param movie_extensions: (tuple) Tuple of all media extensions 83 | """ 84 | if self.extension.upper() in movie_extensions: 85 | self.is_media = True 86 | self.is_sub = False 87 | else: 88 | self.is_sub = True 89 | self.is_media = False 90 | 91 | def __str__(self): 92 | return "Path:\t{0.path}\nParent:\t{0.parent}\n".format(self) 93 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtWidgets 2 | 3 | import sys 4 | import os 5 | 6 | from ui.bindings import SubCrawl 7 | 8 | 9 | sys._excepthook = sys.excepthook 10 | 11 | 12 | # Custom except hook used to detect program errors when PyQt crashes without error messages. 13 | def my_exception_hook(exctype, value, traceback): 14 | # Print the error and traceback 15 | print(exctype, value, traceback) 16 | # Call the normal Exception hook after 17 | sys._excepthook(exctype, value, traceback) 18 | sys.exit(1) 19 | 20 | 21 | sys.excepthook = my_exception_hook 22 | 23 | 24 | def bind_all_buttons(application): 25 | # Binds the signals to the buttons 26 | application.bind_download_button() 27 | application.bind_browse_button() 28 | application.bind_scan_button() 29 | application.bind_clear_button() 30 | application.bind_radio_buttons() 31 | application.bind_combo_box() 32 | application.bind_confirm_selection() 33 | application.bind_cancel_selection() 34 | application.bind_table_selection_changed() 35 | application.bind_remove_entry() 36 | application.populate_language_combo_box() 37 | 38 | 39 | if __name__ == "__main__": 40 | app = QtWidgets.QApplication(sys.argv) 41 | window = SubCrawl() 42 | 43 | # Sets the default home directory to Desktop 44 | desktop_directory = os.path.join(os.environ["HOMEPATH"], "Desktop") 45 | window.SelectedFolderDisplay.setText(desktop_directory) 46 | bind_all_buttons(window) 47 | window.show() 48 | 49 | try: 50 | sys.exit(app.exec_()) 51 | except: 52 | print("Exiting") 53 | 54 | -------------------------------------------------------------------------------- /media.py: -------------------------------------------------------------------------------- 1 | import re 2 | import requests 3 | import json 4 | import os 5 | import PTN # "parse_torrent_name" package 6 | 7 | 8 | class Media(object): 9 | 10 | def __init__(self, file_path: str): 11 | """ 12 | An object which represents some form of media file. Most of the data attributes are pretty self- 13 | explanatory. 14 | 15 | :param file_path: (string) an absolute path of the media file 16 | """ 17 | self.path = file_path 18 | self.folder_name, self.file_name = os.path.split(self.path) 19 | self.title, self.extension = os.path.splitext(self.file_name) 20 | self.subtitles = False 21 | self.sub_path = () 22 | self.sub_language = [] 23 | self.id = None 24 | self.imdb_rating = 0 25 | self.year = "" 26 | 27 | def add_subs(self, sub_path: tuple): 28 | """ 29 | Adds the passed folder as the subtitle path for this instance of the Media object. 30 | 31 | :param sub_path: (tuple) a tuple of absolute paths of the subtitle file/s 32 | """ 33 | self.subtitles = True 34 | self.sub_path = sub_path 35 | 36 | def search_imdb_id(self) -> bool: 37 | """ 38 | Searches through the OMDb API with the API key for a title and year. Returns a response 39 | which is transformed into text form (JSON) 40 | 41 | :return: (bool) Value if the response of the query was received. 42 | TODO: Make the requests an asynchronous operation. 43 | """ 44 | media_type = "movie" 45 | url = "http://www.omdbapi.com/?apikey=678bc96c&t={0.title}&y={0.year}&type={1}".format(self, media_type) 46 | # Checks for internet connection 47 | response = requests.get(url) 48 | media_info = json.loads(response.text) 49 | 50 | try: 51 | self.id = int(media_info["imdbID"].replace("t", "")) 52 | self.title = media_info["Title"] 53 | self.year = media_info["Year"] 54 | self.imdb_rating = [item["Value"] for item in media_info["Ratings"] if item["Source"] == 55 | "Internet Movie Database"][0] 56 | except KeyError: 57 | # TODO: Consider adding a log of movies that were not found 58 | self.id = None 59 | finally: 60 | return media_info["Response"] 61 | 62 | def __str__(self) -> str: 63 | """ 64 | String representation of the object instance. 65 | """ 66 | return "ID: {0.id}\nName: {0.file_name}\nPath: {0.path}\nTitle: {0.title}\nFile type: {0.extension}" \ 67 | "\nSubtitles: {0.subtitles}\nSubtitle language: {0.sub_language}\n\ 68 | Subtitle location: {0.sub_path}\n\n".format(self) 69 | 70 | 71 | class Movie(Media): 72 | 73 | def __init__(self, file_path: str): 74 | super().__init__(file_path=file_path) 75 | 76 | def add_subs(self, sub_path: tuple): 77 | super().add_subs(sub_path=sub_path) 78 | 79 | def extract_movie_info(self): 80 | """ 81 | If package PTN (parse torrent name) fails to detect a title and a year, a batch of homemade regular 82 | expressions are deployed to give it a try. 83 | """ 84 | movie_info = PTN.parse(self.title) 85 | try: 86 | self.title = movie_info["title"] 87 | self.year = movie_info["year"] 88 | except KeyError: 89 | self._parse_movie_name() 90 | 91 | def _parse_movie_name(self): 92 | """ 93 | Several regular expressions specifically targeted to find the titles of movies from 94 | irregularly written ones. re.search is used because we want to match the regular expression 95 | throughout the string, not just the beginning that re.match would do. 96 | 97 | The year_match checks for the year after the title of the movie. For now it works only 98 | on movies. Example of titles it works on: 99 | "The Killing of a Sacred Deer.2017.1080p.WEB-DL.H264.AC3-EVO[EtHD]" 100 | "12 Angry Men 1957 1080p BluRay x264 AAC - Ozlem" 101 | "Life.Is.Beautiful.1997.1080p.BluRay.x264.anoXmous" 102 | """ 103 | movie_regex = re.compile(r"(.*?[.| ])(\(\d{4}\)|\d{4}|\[\d{4}\])?([.| ].*)") 104 | if movie_regex.search(self.title) is not None: 105 | try: 106 | self.year = movie_regex.search(self.title).group(2).strip() 107 | except AttributeError: 108 | self.year = "" 109 | finally: 110 | self.title = movie_regex.search(self.title).group(1).strip() 111 | additional_regex = re.compile(r"(.*)(\[.*\])") 112 | if additional_regex.search(self.title) is not None: 113 | self.title = additional_regex.search(self.title).group(1) 114 | 115 | def search_imdb_id(self) -> None or str: 116 | return super().search_imdb_id() 117 | 118 | def __str__(self) -> str: 119 | """ 120 | String representation of the object instance. 121 | """ 122 | return "Title: {0.title}\nYear: {0.year}\nMovie IMDb ID: {0.id}\nFile name: {0.file_name}\n" \ 123 | "Path: {0.path}\nFile type: {0.extension}\nSubtitles: {0.subtitles}\n" \ 124 | "Subtitle language: {0.sub_language}\nSubtitle location: {0.sub_path}\n\n".format(self) 125 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | git+git://github.com/divijbindlish/parse-torrent-name.git#egg=parse-torrent-name 2 | PyQt5==5.15.1 3 | PyQt5-sip==12.8.1 4 | requests==2.21.0 5 | -------------------------------------------------------------------------------- /resources/file-extensions.txt: -------------------------------------------------------------------------------- 1 | .264.3G2.3GP.3GP2.3GPP.3GPP2.3MM.3P2.60D.787.890.AAF.AEC.AECAP.AEGRAPHIC.AEP.AEPX.AET.AETX.AJP.ALE.AMC.AMV.AMX.ANIM.ANX.AQT.ARCUT.ARF.ASF.ASX.AV.AV3.AVB.AVC.AVCHD.AVD.AVE.AVI.AVM.AVP.AVR.AVS.AVS.AVV.AWLIVE.AXM.AXV.BDM.BDMV.BDT2.BDT3.BIX.BLZ.BMC.BMK.BNP.BOX.BS4.BSF.BU.BVR.BYU.CAMPROJ.CAMREC.CAMV.CED.CEL.CINE.CIP.CLK.CLPI.CME.CMMP.CMMTPL.CMPROJ.CMREC.CMV.CPI.CPVC.CREC.CST.CVC.CX3.D2V.D3V.DAD.DASH.DAV.DB2.DCE.DCK.DCR.DCR.DDAT.DIF.DIVX.DLX.DMB.DMSD.DMSD3D.DMSM.DMSM3D.DMSS.DMX.DNC.DPA.DPG.DREAM.DSY.DV.DV-AVI.DV4.DVDMEDIA.DVR.DVR-MS.DVX.DXR.DZM.DZP.DZT.EDL.EVO.EVO.EXO.EXP.EYE.EYETV.EZT.F4F.F4M.F4P.F4V.FBR.FBR.FBZ.FCARCH.FCP.FCPROJECT.FFD.FFM.FLC.FLH.FLI.FLIC.FLV.FLX.FPDX.FTC.FVT.G2M.G64.G64X.GCS.GFP.GIFV.GL.GOM.GRASP.GTS.GVI.GVP.GXF.H264H.264.HDMOV.HDV.HEVC.HKM.IFO.IMOVIELIBRARY.IMOVIEMOBILE.IMOVIEPROJ.IMOVIEPROJECT.INP.INT.IRCP.IRFH.264.ISM.ISMC.ISMCLIP.ISMV.IVA.IVF.IVR.IVS.IZZ.IZZY.JDR.JMV.JNR.JSS.JTS.JTV.K3G.KDENLIVE.KMV.KTN.LREC.LRV.LSF.LSX.LVIX.M15.M1PG.M1V.M21.M21.M2A.M2P.M2T.M2TS.M2V.M4E.M4U.M4V.M75.MANI.META.MGV.MJ2.MJP.MJPEG.MJPG.MK3D.MKV.MMV.MNV.MOB.MOD.MODD.MOFF.MOI.MOOV.MOV.MOVIE.MOVIE.MP21.MP21.MP2V.MP4.MP4.INFOVID.MP4V.MPE.MPEG.MPEG1.MPEG2.MPEG4.MPF.MPG.MPG2.MPG4.MPGINDEX.MPL.MPL.MPLS.MPROJ.MPSUB.MPV.MPV2.MQV.MSDVD.MSE.MSH.MSWMM.MT2S.MTS.MTV.MVB.MVC.MVD.MVE.MVEX.MVP.MVP.MVY.MXF.MXV.MYS.N3R.NCOR.NFV.NSV.NTP.NUT.NUV.NVC.OGM.OGV.OGX.ORV.OSP.OTRKEY.PAC.PAR.PDS.PGI.PHOTOSHOW.PIV.PJS.PLAYLIST.PLPROJ.PMF.PMV.PNS.PPJ.PREL.PRO.PRO4DVD.PRO5DVD.PROQC.PRPROJ.PRTL.PSB.PSH.PSSD.PSV.PVA.PVR.PXV.PZ.QT.QTCH.QTINDEX.QTL.QTM.QTZ.R3D.RCD.RCPROJECT.RCREC.RCUT.RDB.REC.RM.RMD.RMD.RMP.RMS.RMV.RMVB.ROQ.RP.RSX.RTS.RTS.RUM.RV.RVID.RVL.SAN.SBK.SBT.SBZ.SCC.SCM.SCM.SCN.SCREENFLOW.SDV.SEC.SEC.SEDPRJ.SEQ.SER.SFD.SFERA.SFVIDCAP.SIV.SMI.SMI.SMIL.SMK.SML.SMV.SNAGPROJ.SPL.SQZ.SSF.SSM.STL.STR.STX.SVI.SWF.SWI.SWT.TDA3MT.TDT.TDX.THEATER.THP.TID.TIVO.TIX.TOD.TP.TP0.TPD.TPR.TREC.TRP.TSP.TSV.TTXT.TVLAYER.TVRECORDING.TVS.TVSHOW.USF.USM.V264.VBC.VC1.VCPF.VCR.VCV.VDO.VDR.VDX.VEG.VEM.VEP.VF.VFT.VFW.VFZ.VGZ.VID.VIDEO.VIEWLET.VIV.VIVO.VIX.VLAB.VMLF.VMLT.VOB.VP3.VP6.VP7.VPJ.VR.VRO.VS4.VSE.VSP.VTT.W32.WCP.WEBM.WFSP.WGI.WLMP.WM.WMD.WMMP.WMV.WMX.WOT.WP3.WPL.WSVE.WTV.WVE.WVM.WVX.WXP2.0.XEJ.XEL.XESC.XFL.XLMV.XMV.XVID.Y4M.YOG.YUV.ZEG.ZM1.ZM2.ZM3.ZMV -------------------------------------------------------------------------------- /resources/iso 639 2.json: -------------------------------------------------------------------------------- 1 | [{"Alpha2_Code": "sq", "Alpha3b_Code": "alb", "Alpha3t_Code": "sqi", "English_Name": "Albanian", "French_Name": "albanais"},{"Alpha2_Code": "ar", "Alpha3b_Code": "ara", "Alpha3t_Code": null, "English_Name": "Arabic", "French_Name": "arabe"},{"Alpha2_Code": "hy", "Alpha3b_Code": "arm", "Alpha3t_Code": "hye", "English_Name": "Armenian", "French_Name": "arm\u00e9nien"},{"Alpha2_Code": "az", "Alpha3b_Code": "aze", "Alpha3t_Code": null, "English_Name": "Azerbaijani", "French_Name": "az\u00e9ri"},{"Alpha2_Code": "eu", "Alpha3b_Code": "baq", "Alpha3t_Code": "eus", "English_Name": "Basque", "French_Name": "basque"},{"Alpha2_Code": "be", "Alpha3b_Code": "bel", "Alpha3t_Code": null, "English_Name": "Belarusian", "French_Name": "bi\u00e9lorusse"},{"Alpha2_Code": "bn", "Alpha3b_Code": "ben", "Alpha3t_Code": null, "English_Name": "Bengali", "French_Name": "bengali"},{"Alpha2_Code": "bs", "Alpha3b_Code": "bos", "Alpha3t_Code": null, "English_Name": "Bosnian", "French_Name": "bosniaque"},{"Alpha2_Code": "bg", "Alpha3b_Code": "bul", "Alpha3t_Code": null, "English_Name": "Bulgarian", "French_Name": "bulgare"},{"Alpha2_Code": "ca", "Alpha3b_Code": "cat", "Alpha3t_Code": null, "English_Name": "Catalan", "French_Name": "catalan; valencien"},{"Alpha2_Code": "zh", "Alpha3b_Code": "chi", "Alpha3t_Code": "zho", "English_Name": "Chinese", "French_Name": "chinois"},{"Alpha2_Code": "cs", "Alpha3b_Code": "cze", "Alpha3t_Code": "ces", "English_Name": "Czech", "French_Name": "tch\u00e8que"},{"Alpha2_Code": "da", "Alpha3b_Code": "dan", "Alpha3t_Code": null, "English_Name": "Danish", "French_Name": "danois"},{"Alpha2_Code": "nl", "Alpha3b_Code": "dut", "Alpha3t_Code": "nld", "English_Name": "Dutch", "French_Name": "n\u00e9erlandais; flamand"},{"Alpha2_Code": "en", "Alpha3b_Code": "eng", "Alpha3t_Code": null, "English_Name": "English", "French_Name": "anglais"},{"Alpha2_Code": "eo", "Alpha3b_Code": "epo", "Alpha3t_Code": null, "English_Name": "Esperanto", "French_Name": "esp\u00e9ranto"},{"Alpha2_Code": "et", "Alpha3b_Code": "est", "Alpha3t_Code": null, "English_Name": "Estonian", "French_Name": "estonien"},{"Alpha2_Code": null, "Alpha3b_Code": "fil", "Alpha3t_Code": null, "English_Name": "Filipino", "French_Name": "filipino; pilipino"},{"Alpha2_Code": "fi", "Alpha3b_Code": "fin", "Alpha3t_Code": null, "English_Name": "Finnish", "French_Name": "finnois"},{"Alpha2_Code": "fr", "Alpha3b_Code": "fre", "Alpha3t_Code": "fra", "English_Name": "French", "French_Name": "fran\u00e7ais"},{"Alpha2_Code": "ka", "Alpha3b_Code": "geo", "Alpha3t_Code": "kat", "English_Name": "Georgian", "French_Name": "g\u00e9orgien"},{"Alpha2_Code": "de", "Alpha3b_Code": "ger", "Alpha3t_Code": "deu", "English_Name": "German", "French_Name": "allemand"},{"Alpha2_Code": "ga", "Alpha3b_Code": "gle", "Alpha3t_Code": null, "English_Name": "Irish", "French_Name": "irlandais"},{"Alpha2_Code": "el", "Alpha3b_Code": "gre", "Alpha3t_Code": "ell", "English_Name": "Greek", "French_Name": "grec"},{"Alpha2_Code": "he", "Alpha3b_Code": "heb", "Alpha3t_Code": null, "English_Name": "Hebrew", "French_Name": "h\u00e9breu"},{"Alpha2_Code": "hi", "Alpha3b_Code": "hin", "Alpha3t_Code": null, "English_Name": "Hindi", "French_Name": "hindi"},{"Alpha2_Code": null, "Alpha3b_Code": "hmn", "Alpha3t_Code": null, "English_Name": "Hmong", "French_Name": "hmong"},{"Alpha2_Code": "hr", "Alpha3b_Code": "hrv", "Alpha3t_Code": null, "English_Name": "Croatian", "French_Name": "croate"},{"Alpha2_Code": "hu", "Alpha3b_Code": "hun", "Alpha3t_Code": null, "English_Name": "Hungarian", "French_Name": "hongrois"},{"Alpha2_Code": "is", "Alpha3b_Code": "ice", "Alpha3t_Code": "isl", "English_Name": "Icelandic", "French_Name": "islandais"},{"Alpha2_Code": "id", "Alpha3b_Code": "ind", "Alpha3t_Code": null, "English_Name": "Indonesian", "French_Name": "indon\u00e9sien"},{"Alpha2_Code": "it", "Alpha3b_Code": "ita", "Alpha3t_Code": null, "English_Name": "Italian", "French_Name": "italien"},{"Alpha2_Code": "ja", "Alpha3b_Code": "jpn", "Alpha3t_Code": null, "English_Name": "Japanese", "French_Name": "japonais"},{"Alpha2_Code": "kk", "Alpha3b_Code": "kaz", "Alpha3t_Code": null, "English_Name": "Kazakh", "French_Name": "kazakh"},{"Alpha2_Code": "ko", "Alpha3b_Code": "kor", "Alpha3t_Code": null, "English_Name": "Korean", "French_Name": "cor\u00e9en"},{"Alpha2_Code": "lv", "Alpha3b_Code": "lav", "Alpha3t_Code": null, "English_Name": "Latvian", "French_Name": "letton"},{"Alpha2_Code": "lt", "Alpha3b_Code": "lit", "Alpha3t_Code": null, "English_Name": "Lithuanian", "French_Name": "lituanien"},{"Alpha2_Code": "mk", "Alpha3b_Code": "mac", "Alpha3t_Code": "mkd", "English_Name": "Macedonian", "French_Name": "mac\u00e9donien"},{"Alpha2_Code": "no", "Alpha3b_Code": "nor", "Alpha3t_Code": null, "English_Name": "Norwegian", "French_Name": "norv\u00e9gien"},{"Alpha2_Code": "pa", "Alpha3b_Code": "pan", "Alpha3t_Code": null, "English_Name": "Panjabi; Punjabi", "French_Name": "pendjabi"},{"Alpha2_Code": "fa", "Alpha3b_Code": "per", "Alpha3t_Code": "fas", "English_Name": "Persian", "French_Name": "persan"},{"Alpha2_Code": "pt", "Alpha3b_Code": "por", "Alpha3t_Code": null, "English_Name": "Portuguese", "French_Name": "portugais"},{"Alpha2_Code": "ro", "Alpha3b_Code": "rum", "Alpha3t_Code": "ron", "English_Name": "Romanian", "French_Name": "roumain"},{"Alpha2_Code": "ru", "Alpha3b_Code": "rus", "Alpha3t_Code": null, "English_Name": "Russian", "French_Name": "russe"},{"Alpha2_Code": "sk", "Alpha3b_Code": "slo", "Alpha3t_Code": "slk", "English_Name": "Slovak", "French_Name": "slovaque"},{"Alpha2_Code": "sl", "Alpha3b_Code": "slv", "Alpha3t_Code": null, "English_Name": "Slovenian", "French_Name": "slov\u00e8ne"},{"Alpha2_Code": "so", "Alpha3b_Code": "som", "Alpha3t_Code": null, "English_Name": "Somali", "French_Name": "somali"},{"Alpha2_Code": "es", "Alpha3b_Code": "spa", "Alpha3t_Code": null, "English_Name": "Spanish", "French_Name": "espagnol"},{"Alpha2_Code": "sr", "Alpha3b_Code": "srp", "Alpha3t_Code": null, "English_Name": "Serbian", "French_Name": "serbe"},{"Alpha2_Code": "su", "Alpha3b_Code": "sun", "Alpha3t_Code": null, "English_Name": "Sundanese", "French_Name": "soundanais"},{"Alpha2_Code": "sw", "Alpha3b_Code": "swa", "Alpha3t_Code": null, "English_Name": "Swahili", "French_Name": "swahili"},{"Alpha2_Code": "sv", "Alpha3b_Code": "swe", "Alpha3t_Code": null, "English_Name": "Swedish", "French_Name": "su\u00e9dois"},{"Alpha2_Code": null, "Alpha3b_Code": "syr", "Alpha3t_Code": null, "English_Name": "Syriac", "French_Name": "syriaque"},{"Alpha2_Code": "ta", "Alpha3b_Code": "tam", "Alpha3t_Code": null, "English_Name": "Tamil", "French_Name": "tamoul"},{"Alpha2_Code": "te", "Alpha3b_Code": "tel", "Alpha3t_Code": null, "English_Name": "Telugu", "French_Name": "t\u00e9lougou"},{"Alpha2_Code": "tl", "Alpha3b_Code": "tgl", "Alpha3t_Code": null, "English_Name": "Tagalog", "French_Name": "tagalog"},{"Alpha2_Code": "th", "Alpha3b_Code": "tha", "Alpha3t_Code": null, "English_Name": "Thai", "French_Name": "tha\u00ef"},{"Alpha2_Code": "tr", "Alpha3b_Code": "tur", "Alpha3t_Code": null, "English_Name": "Turkish", "French_Name": "turc"},{"Alpha2_Code": "uk", "Alpha3b_Code": "ukr", "Alpha3t_Code": null, "English_Name": "Ukrainian", "French_Name": "ukrainien"},{"Alpha2_Code": "vi", "Alpha3b_Code": "vie", "Alpha3t_Code": null, "English_Name": "Vietnamese", "French_Name": "vietnamien"},{"Alpha2_Code": "cy", "Alpha3b_Code": "wel", "Alpha3t_Code": "cym", "English_Name": "Welsh", "French_Name": "gallois"},{"Alpha2_Code": "wa", "Alpha3b_Code": "wln", "Alpha3t_Code": null, "English_Name": "Walloon", "French_Name": "wallon"},{"Alpha2_Code": "xh", "Alpha3b_Code": "xho", "Alpha3t_Code": null, "English_Name": "Xhosa", "French_Name": "xhosa"}] -------------------------------------------------------------------------------- /scanner.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from folder import Folder, File 5 | from media import Movie 6 | from db_interactor import _DBInteractor 7 | 8 | 9 | class Scanner(object): 10 | 11 | def __init__(self, path: str, program_dir: str): 12 | """ 13 | :param program_dir: (string) Specifies the directory in which the program is installed 14 | :param path: (string) absolute path of the folder which to scan 15 | """ 16 | self.path = os.path.abspath(path) 17 | self._program_dir = program_dir 18 | self.interactor = _DBInteractor(self._program_dir) 19 | self.movie_extensions = self._get_media_files_extensions() 20 | 21 | self.current_folder = None 22 | self.current_file = None 23 | 24 | def _get_media_files_extensions(self) -> tuple: 25 | """ 26 | Pulls out from a text file a list of all media file extensions 27 | 28 | :return movie_extension: (tuple) list of strings with the file extensions 29 | """ 30 | os.chdir(self._program_dir) 31 | 32 | with open("resources\\file-extensions.txt", "r") as f: 33 | movie_extensions = re.findall(r"(\.\w*)", f.read()) 34 | return tuple(movie_extensions) 35 | 36 | def perform_scan(self, progress_tuple: tuple): 37 | """ 38 | Performs the necessary steps to scan the designated folder. 39 | 40 | :param progress_tuple: (tuple) -> (function, integer) tuple that contains a function to update a progress bar 41 | and the number of total files in the selected folder 42 | """ 43 | self._scan_folder(self.path, progress_tuple) 44 | self.interactor.commit_and_renew_cursor() 45 | 46 | def get_number_of_duplicate_files(self): 47 | return self.interactor.duplicate_files 48 | 49 | def _scan_folder(self, selected_folder: str, progress_tuple: tuple): 50 | """ 51 | Walks through the given folder for any media file. When the media file is found, it is saved 52 | into a database. Once a database is created there is no need to repopulate it during every scan 53 | if the file still exists. The file is removed from the database if during the scan it is not in 54 | the folder it used to be. Same as if a new file appeared that was not there before. 55 | 56 | :param selected_folder: (string) path to the folder which is scanned 57 | :param progress_tuple: (tuple) -> (function, integer) tuple that contains a function to update a progress bar 58 | and the number of total files in the selected folder 59 | """ 60 | os.chdir(selected_folder) 61 | scanned_files = 0 62 | 63 | for folder_name, _, file_names in os.walk(selected_folder): 64 | self.current_folder = Folder(folder_name) 65 | 66 | for file_name in file_names: 67 | scanned_files += 1 68 | self._update_scanning_progress_bar(scanned_files, progress_tuple) 69 | self._create_children_for_current_folder(file_name) 70 | 71 | self._pair_media_and_subs() 72 | 73 | # Return back to the directory of the program 74 | os.chdir(self._program_dir) 75 | 76 | def _update_scanning_progress_bar(self, scanned_files: int, progress_tuple: tuple): 77 | """ 78 | A number of scanned files is passed, and a function that will update the progress bar (GUI) with the scanned 79 | number of files compared to the total. 80 | 81 | :param scanned_files: (integer) number of files scanned in the selected folder 82 | :param progress_tuple: (tuple) -> (function, integer) tuple that contains a function to update a progress bar 83 | and the number of total files in the selected folder 84 | """ 85 | update_fn = progress_tuple[0] 86 | total_files = progress_tuple[1] 87 | percent = round((scanned_files / total_files) * 100, 2) 88 | update_fn(percent) 89 | 90 | def _create_children_for_current_folder(self, file_name): 91 | """ 92 | If the file is a media file or if the file is a subtitle file it creates children for the 93 | "current_folder" Folder type. 94 | """ 95 | if file_name.upper().endswith(self.movie_extensions) or file_name.upper().endswith((".RAR", ".ZIP", ".SRT")): 96 | current_file_name = os.path.join(self.current_folder.path, file_name) 97 | self.current_file = File(current_file_name) 98 | self.current_file.detect_media_or_sub(self.movie_extensions) 99 | self.current_folder.add_child(self.current_file) 100 | 101 | def _pair_media_and_subs(self): 102 | """ 103 | Creates a tuple of subtitle file absolute paths and pairs them with the corresponding media file to the method 104 | which creates them into an object. 105 | """ 106 | self.current_folder.lock_children() 107 | media_contains_subs = tuple([file.path for file in self.current_folder.children if file.is_sub]) 108 | for file in self.current_folder.children: 109 | if file.is_media: 110 | self._create_media(file.path, media_contains_subs) 111 | 112 | def _create_media(self, file_path: str, media_contains_subs: tuple, table="all_movies"): 113 | """ 114 | Creates a Media object with the provided file_name and folder_name. If the media is a movie then creates 115 | a Movie object, if it is a series it creates a Series object. Adds it to the table all_movies. 116 | TODO: Add Series object to media.py 117 | 118 | :param file_path: (string) absolute path of the current file 119 | :param media_contains_subs: (tuple) indicates if the media has subtitles by getting an absolute path of the subs as a tuple 120 | :param table: (string) table to which the object will be added 121 | """ 122 | media = Movie(file_path) 123 | media.extract_movie_info() 124 | # movie_found = True # No internet connection 125 | movie_found = media.search_imdb_id() 126 | if movie_found: 127 | if media_contains_subs: 128 | media.add_subs(media_contains_subs) 129 | self.interactor.add_media_to_db(media, table) 130 | -------------------------------------------------------------------------------- /subtitles.py: -------------------------------------------------------------------------------- 1 | import json 2 | import gzip 3 | import os 4 | import base64 5 | from socket import gaierror 6 | from http.client import ResponseNotReady 7 | from xmlrpc.client import ServerProxy, ProtocolError, Fault, expat 8 | 9 | 10 | class SubtitlePreference(object): 11 | """ 12 | Saves the users preferences for subtitle downloading, be it a selected language or sources from which to download 13 | selected subtitles. 14 | """ 15 | 16 | def __init__(self): 17 | """ 18 | Defaults the language to Albanian if the user does not select any language. 19 | """ 20 | self.language_name = "Albanian" 21 | self.language_iso2 = "sq" 22 | self.language_iso3 = "alb" 23 | self.sub_source_preference = ("OpenSubtitles", "SubDB") 24 | 25 | def add_language(self, language_preference: str): 26 | """ 27 | Adds the selected language to the class from a file which contains the list of all ISO639 languages. 28 | 29 | :param language_preference: (string) selected language from the combo box on the UI 30 | """ 31 | with open("resources/iso 639 2.json", "r") as languages_file: 32 | languages_json = json.load(languages_file) 33 | for language in languages_json: 34 | if language_preference == language["English_Name"]: 35 | self.language_name = language["English_Name"] 36 | self.language_iso2 = language["Alpha2_Code"] 37 | self.language_iso3 = language["Alpha3b_Code"] 38 | 39 | def change_sub_source(self, sub_source_list: list): 40 | """ 41 | Changes the source of subtitle downloading depending on what the user ticked in the checkbox in the GUI. The 42 | tuple will be either one element or two elements long. 43 | TODO: Add to use 44 | 45 | :param sub_source_list: (list) list containing sources 46 | """ 47 | self.sub_source_preference = tuple(sub_source_list) 48 | 49 | def __str__(self): 50 | return "Subtitle language preference:\t{0.language_name} - {0.language_iso2} - {0.language_iso3}\n" \ 51 | "Subtitle sources preference: {0.sub_source_preference}\n".format(self) 52 | 53 | 54 | class SubtitleDownloader(object): 55 | 56 | """ 57 | Class for downloading subtitles. It's data attributes contain various information necessary for clarity and 58 | easy access. 59 | The class is instantiated with the preferences needed to operate (language preference, download sources, prompt 60 | to display information on and progress bar which to update). Then the downloading method is called which in turn 61 | does all the heavy lifting. 62 | """ 63 | 64 | def __init__(self, subtitle_preference: SubtitlePreference, prompt_label, progress_bar, interactor): 65 | """ 66 | :param subtitle_preference: (SubtitlePreference) signals from which sources to download and in what language 67 | :param prompt_label: (PromptLabel) label to which information during downloading will be displayed 68 | :param progress_bar: (ProgressBar) progress bar which is updated with the progression of downloading 69 | :param interactor: (_DB_Interactor) interacts with the database to retrieve information 70 | """ 71 | self.preference = subtitle_preference 72 | self.prompt_label = prompt_label 73 | self.progress_bar = progress_bar 74 | self.interactor = interactor 75 | self.downloaded_files = 0 76 | 77 | # Token to log in to OpenSubtitles 78 | self.opensubs_token = None 79 | self.sub_file_extensions = (".RAR", ".ZIP", ".SRT") 80 | 81 | def _create_payload_for_subtitle_searching(self, entry: tuple) -> dict: 82 | """ 83 | Creates a payload consisting of IMDbID, movie title and subtitle language data ready for downloading. 84 | 85 | :param entry: (tuple) tuple consisting of fields of a record from the database 86 | :return payload: (dictionary) information crucial for subtitle downloading for that particular movie 87 | """ 88 | try: 89 | entry_id = entry[0] 90 | entry_title = entry[4] 91 | movie_directory, _ = os.path.split(entry[2]) 92 | except KeyError: 93 | payload_for_sub_search = dict() 94 | else: 95 | # If "imdbid" is defined, "query" is ignored. 96 | payload_for_sub_search = {"imdbid": entry_id, 97 | "query": entry_title, 98 | "sublanguageid": self.preference.language_iso3, 99 | "movie directory": movie_directory} 100 | return payload_for_sub_search 101 | 102 | def _perform_query_and_store(self, payload_for_sub_search: dict, proxy: ServerProxy): 103 | """ 104 | Searches for the desired subtitles through the OpenSubtitles API and writes the download URL information 105 | to a table ("search_subs"). 106 | 107 | :param payload_for_sub_search: (dictionary) contains the information about the movie for which 108 | the subtitle will download 109 | :param proxy: ServerProxy.LogIn(username, password, language, useragent) 110 | 111 | :return download_data: (dictionary) contains crucial information for file downloading 112 | """ 113 | try: 114 | query_result = proxy.SearchSubtitles(self.opensubs_token, [payload_for_sub_search], {"limit": 10}) 115 | except Fault: 116 | self.prompt_label.setText("A fault has occurred") 117 | except ProtocolError: 118 | self.prompt_label.setText("A ProtocolError has occurred.") 119 | else: 120 | if query_result["status"] == "200 OK": 121 | if query_result["data"]: 122 | payload_for_download = self._create_download_data(query_result["data"], payload_for_sub_search) 123 | self.interactor.add_subtitle_search_data_to_db(payload_for_download) 124 | else: 125 | self.prompt_label.setText("There is no subtitles in this language for {}". 126 | format(payload_for_sub_search["query"])) 127 | else: 128 | self.prompt_label.setText("Wrong status code: {}".format(query_result["status"])) 129 | 130 | def _create_download_data(self, query_results: dict, payload_for_sub_search: dict): 131 | """ 132 | Creates the subtitle download data from the results of the OpenSubtitles server query 133 | 134 | :param query_results: (list) list of dictionaries containing information regarding queried subtitles 135 | :param payload_for_sub_search: (dict) payload created for the OpenSubtitles server query 136 | """ 137 | for result in query_results: 138 | subtitle_name, download_link, sub_id = result["SubFileName"], result["SubDownloadLink"], result["IDSubtitleFile"] 139 | movie_id = payload_for_sub_search["imdbid"] 140 | movie_directory = payload_for_sub_search["movie directory"] 141 | if subtitle_name.upper().endswith(self.sub_file_extensions): 142 | payload_for_download = {"imdbid": movie_id, 143 | "file name": subtitle_name, 144 | "IDSubtitleFile": sub_id, 145 | "movie directory": movie_directory} 146 | return payload_for_download 147 | 148 | def _perform_file_download(self, proxy): 149 | """ 150 | Creates a list of subtitle file ID's that the user has selected to download. Those ID's are passed to a function 151 | which will download the subtitle file byte code data and save to a file in the movie directory in chunks of 152 | 20 files at a time (OpenSubtitle API restriction). 153 | 154 | :param proxy: (ServerProxy) 155 | """ 156 | # Get subtitle information to download 157 | subtitle_ids = [sub_id for sub_id, _, __, ___ in self.interactor.retrieve("search_subs")] 158 | while len(subtitle_ids) >= 19: 159 | self._download_file(proxy, subtitle_ids[:19]) 160 | subtitle_ids = subtitle_ids[19:] 161 | print(len(subtitle_ids)) 162 | if subtitle_ids: 163 | self._download_file(proxy, subtitle_ids) 164 | 165 | def _download_file(self, proxy, subtitle_ids): 166 | """ 167 | Tries to download byte data. If successful the data will be stored to a table in the database. Afterwards, that 168 | same data will be taken from that table and another table and written to a file. 169 | """ 170 | download_data = dict() 171 | try: 172 | download_data = proxy.DownloadSubtitles(self.opensubs_token, subtitle_ids) 173 | except ProtocolError as e: 174 | download_data["status"] = e 175 | self.prompt_label.setText("There has been a ProtocolError during downloading") 176 | except ResponseNotReady as e: 177 | download_data["status"] = e 178 | self.prompt_label.setText("There has been a ResponseNotReady Error during downloading") 179 | 180 | if download_data["status"] == "200 OK": 181 | self._store_byte_data_to_db(download_data) 182 | self._get_stored_byte_data() 183 | else: 184 | self.prompt_label.setText("There was an error while trying to download your file: {}" 185 | .format(download_data["status"])) 186 | 187 | def _store_byte_data_to_db(self, download_data): 188 | for individual_download_dict in download_data["data"]: 189 | self.interactor.add_subtitle_download_data_to_db(tuple(individual_download_dict.values())) 190 | self.interactor.commit_and_renew_cursor() 191 | 192 | def _get_stored_byte_data(self): 193 | for sub_id, byte_data in self.interactor.retrieve("download_subs"): 194 | search_condition = ("subs_id", sub_id) 195 | # Get subtitle file name and movie directory path from another table 196 | for _, __, sub_name, movie_directory in self.interactor.retrieve("search_subs", search_condition): 197 | subtitle_path = movie_directory + "\\" + sub_name + ".gzip" 198 | self._write_file(byte_data, subtitle_path) 199 | break 200 | 201 | def _write_file(self, byte_data: str, subtitle_path: str): 202 | """ 203 | Encode the byte_data string to bytes (since it's not in byte format by default) and write it to a .gzip 204 | file. Unzip the content of the .gzip file and write it outside (unzipped). 205 | 206 | :param byte_data: (string) string containing bytecode information 207 | ATTENTION: variable is not byte encoded, which is why it is done in this method 208 | :param subtitle_path: (string) absolute path where to write the subtitle 209 | """ 210 | with open(subtitle_path, "wb") as subtitle_file: 211 | subtitle_file.write(base64.decodebytes(byte_data.encode())) 212 | 213 | # Open and read the compressed file and write it outside 214 | with gzip.open(subtitle_path, 'rb') as gzip_file: 215 | content = gzip_file.read() 216 | # Removes the ".gzip" extension 217 | with open(subtitle_path[:-4], 'wb') as srt_file: 218 | srt_file.write(content) 219 | 220 | self.downloaded_files += 1 221 | # Remove the .gzip file 222 | os.remove(subtitle_path) 223 | 224 | def download_from_opensubtitles(self): 225 | """ 226 | Logs the user into the OpenSubtitles API. If the log in is successful then payloads are created for querying 227 | the OpenSubtitles database. The query result is passed to the download function. Meanwhile the Prompt Label in 228 | the GUI is updated with information. 229 | After all the downloading and querying is finished, the user is logged out. 230 | """ 231 | with ServerProxy("https://api.opensubtitles.org/xml-rpc") as proxy: 232 | self.opensubs_token = self.log_in_opensubtitles(proxy) 233 | 234 | if self.opensubs_token != "error": 235 | self.prompt_label.setText("Connected to OpenSubtitles database") 236 | 237 | for payload_for_sub_search in (self._create_payload_for_subtitle_searching(entry) 238 | for entry in self.interactor.retrieve("selected_movies")): 239 | # Removes the movie directory path from the payload which will be sent to OpenSubtitles 240 | self._perform_query_and_store(payload_for_sub_search, proxy) 241 | 242 | self.interactor.commit_and_renew_cursor() 243 | 244 | self._perform_file_download(proxy) 245 | 246 | self.interactor.clear_db("search_subs") 247 | self.interactor.clear_db("download_subs") 248 | 249 | self.prompt_label.setText("Finishing up ...") 250 | proxy.LogOut(self.opensubs_token) 251 | self.prompt_label.setText("Download finished! Downloaded {} files".format(self.downloaded_files)) 252 | 253 | self.downloaded_files = 0 254 | 255 | def update_progress(self, chunk_size: int, progress_tuple: tuple): 256 | """ 257 | A number of scanned files is passed, and a function that will update the progress bar (GUI) with the scanned 258 | number of files compared to the total. 259 | 260 | :param chunk_size: (integer) number of files scanned in the selected folder 261 | :param progress_tuple: (tuple) -> (function, integer) tuple that contains a function to update a progress bar 262 | and the number of total files in the selected folder 263 | """ 264 | update_fn = progress_tuple[0] 265 | file_size = progress_tuple[1] 266 | percent = round((chunk_size / file_size) * 100, 2) 267 | update_fn(percent) 268 | 269 | def log_in_opensubtitles(self, proxy: ServerProxy) -> str: 270 | """ 271 | Logs in the user to OpenSubtitles. This function should be called always when starting talking with server. 272 | It returns token, which must be used in later communication. If user has no account, blank username and 273 | password should be OK. As language - use ​ISO639 2 letter code. 274 | 275 | :param proxy: ServerProxy.LogIn(username, password, language, useragent) 276 | username: (string) Can be blank since anonymous users are allowed 277 | password: (string) Can be blank since anonymous users are allowed 278 | language: (string) Either HTTP ACCEPT-LANGUAGE header or ISO639 2 279 | useragent: (string) Use your registered useragent, also provide version number - we need tracking 280 | version numbers of your program. If your UA is not registered, you will get error 414 Unknown User Agent 281 | 282 | :return: token or error message 283 | 284 | Link to request useragent: 285 | http://trac.opensubtitles.org/projects/opensubtitles/wiki/DevReadFirst 286 | """ 287 | try: 288 | self.prompt_label.setText("Logging in to OpenSubtitles, please wait ...") 289 | username = "subcrawlproject" 290 | password = "subtitledownloader456" 291 | user_agent = "SubcrawlProjectUserAgent" 292 | login = proxy.LogIn(username, password, self.preference.language_iso3, user_agent) 293 | except Fault: 294 | self.prompt_label.setText("There was a fault while logging in to OpenSubtitles. Please try again.") 295 | return "error" 296 | except ProtocolError: 297 | self.prompt_label.setText("There was an error with the server. Please try again later.") 298 | return "error" 299 | except ConnectionResetError or ConnectionError or ConnectionAbortedError or ConnectionRefusedError: 300 | self.prompt_label.setText("Please check your internet connection.") 301 | return "error" 302 | except expat.ExpatError: 303 | # https://stackoverflow.com/questions/3664084/xml-parser-syntax-error 304 | self.prompt_label.setText("The received payload is probably incorrect") 305 | return "error" 306 | except gaierror: 307 | self.prompt_label.setText("Please check your internet connection and try again") 308 | return "error" 309 | except Exception as e: 310 | self.prompt_label.setText("Be sure to send us this error: {}".format(str(e))) 311 | return "error" 312 | else: 313 | if login["status"] == "200 OK": 314 | return login["token"] 315 | else: 316 | return "error" 317 | -------------------------------------------------------------------------------- /ui/SubCrawl.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SubCrawl 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1280 10 | 851 11 | 12 | 13 | 14 | 15 | 1 16 | 1 17 | 18 | 19 | 20 | 21 | 1280 22 | 800 23 | 24 | 25 | 26 | 27 | 1280 28 | 851 29 | 30 | 31 | 32 | 33 | Museo Sans For Dell 34 | 9 35 | 36 | 37 | 38 | SubCrawl v0.9 39 | 40 | 41 | Qt::LeftToRight 42 | 43 | 44 | false 45 | 46 | 47 | QTabWidget::Rounded 48 | 49 | 50 | 51 | 52 | 1 53 | 1 54 | 55 | 56 | 57 | 58 | 1280 59 | 800 60 | 61 | 62 | 63 | 64 | 1280 65 | 800 66 | 67 | 68 | 69 | 70 | 71 | 0 72 | 0 73 | 1271 74 | 811 75 | 76 | 77 | 78 | 79 | QLayout::SetDefaultConstraint 80 | 81 | 82 | 10 83 | 84 | 85 | 10 86 | 87 | 88 | 10 89 | 90 | 91 | 10 92 | 93 | 94 | 0 95 | 96 | 97 | 98 | 99 | 5 100 | 101 | 102 | 10 103 | 104 | 105 | 106 | 107 | 108 | 16777215 109 | 15 110 | 111 | 112 | 113 | 114 | Amiri 115 | 8 116 | 117 | 118 | 119 | 0 120 | 121 | 122 | false 123 | 124 | 125 | false 126 | 127 | 128 | QProgressBar::TopToBottom 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 10 137 | 138 | 139 | 140 | QFrame::Raised 141 | 142 | 143 | 1 144 | 145 | 146 | Qt::Horizontal 147 | 148 | 149 | 150 | 151 | 152 | 153 | true 154 | 155 | 156 | 157 | 1249 158 | 399 159 | 160 | 161 | 162 | 163 | 1249 164 | 399 165 | 166 | 167 | 168 | 169 | Museo Sans For Dell 170 | 10 171 | 50 172 | false 173 | 174 | 175 | 176 | QFrame::Box 177 | 178 | 179 | QFrame::Plain 180 | 181 | 182 | 1 183 | 184 | 185 | 1 186 | 187 | 188 | Qt::ScrollBarAsNeeded 189 | 190 | 191 | QAbstractItemView::NoEditTriggers 192 | 193 | 194 | false 195 | 196 | 197 | QAbstractItemView::MultiSelection 198 | 199 | 200 | QAbstractItemView::SelectRows 201 | 202 | 203 | Qt::ElideMiddle 204 | 205 | 206 | true 207 | 208 | 209 | Qt::SolidLine 210 | 211 | 212 | true 213 | 214 | 215 | 0 216 | 217 | 218 | 6 219 | 220 | 221 | true 222 | 223 | 224 | true 225 | 226 | 227 | 55 228 | 229 | 230 | 208 231 | 232 | 233 | true 234 | 235 | 236 | false 237 | 238 | 239 | false 240 | 241 | 242 | 37 243 | 244 | 245 | 246 | IMDb ID 247 | 248 | 249 | 250 | Museo Sans For Dell 251 | 12 252 | 75 253 | true 254 | 255 | 256 | 257 | AlignCenter 258 | 259 | 260 | 261 | 262 | Title 263 | 264 | 265 | 266 | Museo Sans For Dell 267 | 12 268 | 75 269 | true 270 | 271 | 272 | 273 | AlignCenter 274 | 275 | 276 | 277 | 278 | IMDb rating 279 | 280 | 281 | 282 | Museo Sans For Dell 283 | 12 284 | 75 285 | true 286 | 287 | 288 | 289 | AlignCenter 290 | 291 | 292 | 293 | 294 | Year 295 | 296 | 297 | 298 | Museo Sans For Dell 299 | 12 300 | 75 301 | true 302 | 303 | 304 | 305 | AlignCenter 306 | 307 | 308 | 309 | 310 | File location 311 | 312 | 313 | 314 | Museo Sans For Dell 315 | 12 316 | 75 317 | true 318 | 319 | 320 | 321 | AlignCenter 322 | 323 | 324 | 325 | 326 | Subtitles 327 | 328 | 329 | 330 | Museo Sans For Dell 331 | 12 332 | 75 333 | true 334 | 335 | 336 | 337 | AlignCenter 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 0 348 | 349 | 350 | 0 351 | 352 | 353 | 5 354 | 355 | 356 | 357 | 358 | 359 | 360 | 20 361 | 0 362 | 101 363 | 24 364 | 365 | 366 | 367 | 368 | Museo Sans For Dell 369 | 10 370 | 371 | 372 | 373 | Select all 374 | 375 | 376 | false 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | false 385 | 386 | 387 | 388 | 150 389 | 30 390 | 391 | 392 | 393 | 394 | 150 395 | 30 396 | 397 | 398 | 399 | 400 | Museo Sans For Dell 401 | 10 402 | 403 | 404 | 405 | Remove entry 406 | 407 | 408 | 409 | 410 | 411 | 412 | 2 413 | 414 | 415 | Qt::Horizontal 416 | 417 | 418 | 419 | 420 | 421 | 422 | false 423 | 424 | 425 | 426 | 150 427 | 30 428 | 429 | 430 | 431 | 432 | 150 433 | 30 434 | 435 | 436 | 437 | 438 | Museo Sans For Dell 439 | 10 440 | 441 | 442 | 443 | Confirm selection 444 | 445 | 446 | false 447 | 448 | 449 | false 450 | 451 | 452 | false 453 | 454 | 455 | false 456 | 457 | 458 | false 459 | 460 | 461 | 462 | 463 | 464 | 465 | true 466 | 467 | 468 | 469 | 150 470 | 30 471 | 472 | 473 | 474 | 475 | 150 476 | 30 477 | 478 | 479 | 480 | 481 | Museo Sans For Dell 482 | 10 483 | 484 | 485 | 486 | Clear database 487 | 488 | 489 | 490 | 491 | 492 | 493 | false 494 | 495 | 496 | 497 | 150 498 | 30 499 | 500 | 501 | 502 | 503 | 150 504 | 30 505 | 506 | 507 | 508 | 509 | Museo Sans For Dell 510 | 10 511 | 512 | 513 | 514 | Cancel selection 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | Museo Sans For Dell 523 | 10 524 | 525 | 526 | 527 | 528 | 529 | 530 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | Museo Sans For Dell 543 | 12 544 | 50 545 | false 546 | 547 | 548 | 549 | Table view: 550 | 551 | 552 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 6 562 | 12 563 | 154 564 | 24 565 | 566 | 567 | 568 | 569 | Museo Sans For Dell 570 | 10 571 | 572 | 573 | 574 | Show all movies 575 | 576 | 577 | true 578 | 579 | 580 | 581 | 582 | 583 | 6 584 | 43 585 | 241 586 | 24 587 | 588 | 589 | 590 | 591 | Museo Sans For Dell 592 | 10 593 | 594 | 595 | 596 | Show movies with subtitles 597 | 598 | 599 | 600 | 601 | 602 | 6 603 | 74 604 | 268 605 | 24 606 | 607 | 608 | 609 | 610 | Museo Sans For Dell 611 | 10 612 | 613 | 614 | 615 | Show movies without subtitles 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 290 629 | 110 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 0 639 | 0 640 | 0 641 | 642 | 643 | 644 | 645 | 646 | 647 | 80 648 | 80 649 | 80 650 | 651 | 652 | 653 | 654 | 655 | 656 | 120 657 | 120 658 | 120 659 | 660 | 661 | 662 | 663 | 664 | 665 | 100 666 | 100 667 | 100 668 | 669 | 670 | 671 | 672 | 673 | 674 | 40 675 | 40 676 | 40 677 | 678 | 679 | 680 | 681 | 682 | 683 | 53 684 | 53 685 | 53 686 | 687 | 688 | 689 | 690 | 691 | 692 | 0 693 | 0 694 | 0 695 | 696 | 697 | 698 | 699 | 700 | 701 | 255 702 | 255 703 | 255 704 | 705 | 706 | 707 | 708 | 709 | 710 | 255 711 | 255 712 | 255 713 | 714 | 715 | 716 | 717 | 718 | 719 | 0 720 | 0 721 | 0 722 | 723 | 724 | 725 | 726 | 727 | 728 | 80 729 | 80 730 | 80 731 | 732 | 733 | 734 | 735 | 736 | 737 | 0 738 | 0 739 | 0 740 | 741 | 742 | 743 | 744 | 745 | 746 | 40 747 | 40 748 | 40 749 | 750 | 751 | 752 | 753 | 754 | 755 | 255 756 | 255 757 | 220 758 | 759 | 760 | 761 | 762 | 763 | 764 | 0 765 | 0 766 | 0 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 0 776 | 0 777 | 0 778 | 779 | 780 | 781 | 782 | 783 | 784 | 80 785 | 80 786 | 80 787 | 788 | 789 | 790 | 791 | 792 | 793 | 120 794 | 120 795 | 120 796 | 797 | 798 | 799 | 800 | 801 | 802 | 100 803 | 100 804 | 100 805 | 806 | 807 | 808 | 809 | 810 | 811 | 40 812 | 40 813 | 40 814 | 815 | 816 | 817 | 818 | 819 | 820 | 53 821 | 53 822 | 53 823 | 824 | 825 | 826 | 827 | 828 | 829 | 0 830 | 0 831 | 0 832 | 833 | 834 | 835 | 836 | 837 | 838 | 255 839 | 255 840 | 255 841 | 842 | 843 | 844 | 845 | 846 | 847 | 255 848 | 255 849 | 255 850 | 851 | 852 | 853 | 854 | 855 | 856 | 0 857 | 0 858 | 0 859 | 860 | 861 | 862 | 863 | 864 | 865 | 80 866 | 80 867 | 80 868 | 869 | 870 | 871 | 872 | 873 | 874 | 0 875 | 0 876 | 0 877 | 878 | 879 | 880 | 881 | 882 | 883 | 40 884 | 40 885 | 40 886 | 887 | 888 | 889 | 890 | 891 | 892 | 255 893 | 255 894 | 220 895 | 896 | 897 | 898 | 899 | 900 | 901 | 0 902 | 0 903 | 0 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 40 913 | 40 914 | 40 915 | 916 | 917 | 918 | 919 | 920 | 921 | 80 922 | 80 923 | 80 924 | 925 | 926 | 927 | 928 | 929 | 930 | 120 931 | 120 932 | 120 933 | 934 | 935 | 936 | 937 | 938 | 939 | 100 940 | 100 941 | 100 942 | 943 | 944 | 945 | 946 | 947 | 948 | 40 949 | 40 950 | 40 951 | 952 | 953 | 954 | 955 | 956 | 957 | 53 958 | 53 959 | 53 960 | 961 | 962 | 963 | 964 | 965 | 966 | 40 967 | 40 968 | 40 969 | 970 | 971 | 972 | 973 | 974 | 975 | 255 976 | 255 977 | 255 978 | 979 | 980 | 981 | 982 | 983 | 984 | 40 985 | 40 986 | 40 987 | 988 | 989 | 990 | 991 | 992 | 993 | 80 994 | 80 995 | 80 996 | 997 | 998 | 999 | 1000 | 1001 | 1002 | 80 1003 | 80 1004 | 80 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 0 1012 | 0 1013 | 0 1014 | 1015 | 1016 | 1017 | 1018 | 1019 | 1020 | 80 1021 | 80 1022 | 80 1023 | 1024 | 1025 | 1026 | 1027 | 1028 | 1029 | 255 1030 | 255 1031 | 220 1032 | 1033 | 1034 | 1035 | 1036 | 1037 | 1038 | 0 1039 | 0 1040 | 0 1041 | 1042 | 1043 | 1044 | 1045 | 1046 | 1047 | 1048 | 1049 | Museo Sans For Dell 1050 | 46 1051 | 50 1052 | false 1053 | false 1054 | 1055 | 1056 | 1057 | SubCrawl 1058 | 1059 | 1060 | 0 1061 | 1062 | 1063 | 1064 | 1065 | 1066 | 1067 | 1068 | 12 1069 | 1070 | 1071 | 1072 | 1 1073 | 1074 | 1075 | Qt::Vertical 1076 | 1077 | 1078 | 1079 | 1080 | 1081 | 1082 | 1083 | 250 1084 | 110 1085 | 1086 | 1087 | 1088 | 1089 | 1090 | 1091 | 1092 | 1093 | 102 1094 | 102 1095 | 102 1096 | 1097 | 1098 | 1099 | 1100 | 1101 | 1102 | 80 1103 | 80 1104 | 80 1105 | 1106 | 1107 | 1108 | 1109 | 1110 | 1111 | 120 1112 | 120 1113 | 120 1114 | 1115 | 1116 | 1117 | 1118 | 1119 | 1120 | 100 1121 | 100 1122 | 100 1123 | 1124 | 1125 | 1126 | 1127 | 1128 | 1129 | 40 1130 | 40 1131 | 40 1132 | 1133 | 1134 | 1135 | 1136 | 1137 | 1138 | 53 1139 | 53 1140 | 53 1141 | 1142 | 1143 | 1144 | 1145 | 1146 | 1147 | 0 1148 | 0 1149 | 0 1150 | 1151 | 1152 | 1153 | 1154 | 1155 | 1156 | 255 1157 | 255 1158 | 255 1159 | 1160 | 1161 | 1162 | 1163 | 1164 | 1165 | 255 1166 | 255 1167 | 255 1168 | 1169 | 1170 | 1171 | 1172 | 1173 | 1174 | 0 1175 | 0 1176 | 0 1177 | 1178 | 1179 | 1180 | 1181 | 1182 | 1183 | 80 1184 | 80 1185 | 80 1186 | 1187 | 1188 | 1189 | 1190 | 1191 | 1192 | 0 1193 | 0 1194 | 0 1195 | 1196 | 1197 | 1198 | 1199 | 1200 | 1201 | 40 1202 | 40 1203 | 40 1204 | 1205 | 1206 | 1207 | 1208 | 1209 | 1210 | 255 1211 | 255 1212 | 220 1213 | 1214 | 1215 | 1216 | 1217 | 1218 | 1219 | 0 1220 | 0 1221 | 0 1222 | 1223 | 1224 | 1225 | 1226 | 1227 | 1228 | 1229 | 1230 | 102 1231 | 102 1232 | 102 1233 | 1234 | 1235 | 1236 | 1237 | 1238 | 1239 | 80 1240 | 80 1241 | 80 1242 | 1243 | 1244 | 1245 | 1246 | 1247 | 1248 | 120 1249 | 120 1250 | 120 1251 | 1252 | 1253 | 1254 | 1255 | 1256 | 1257 | 100 1258 | 100 1259 | 100 1260 | 1261 | 1262 | 1263 | 1264 | 1265 | 1266 | 40 1267 | 40 1268 | 40 1269 | 1270 | 1271 | 1272 | 1273 | 1274 | 1275 | 53 1276 | 53 1277 | 53 1278 | 1279 | 1280 | 1281 | 1282 | 1283 | 1284 | 0 1285 | 0 1286 | 0 1287 | 1288 | 1289 | 1290 | 1291 | 1292 | 1293 | 255 1294 | 255 1295 | 255 1296 | 1297 | 1298 | 1299 | 1300 | 1301 | 1302 | 255 1303 | 255 1304 | 255 1305 | 1306 | 1307 | 1308 | 1309 | 1310 | 1311 | 0 1312 | 0 1313 | 0 1314 | 1315 | 1316 | 1317 | 1318 | 1319 | 1320 | 80 1321 | 80 1322 | 80 1323 | 1324 | 1325 | 1326 | 1327 | 1328 | 1329 | 0 1330 | 0 1331 | 0 1332 | 1333 | 1334 | 1335 | 1336 | 1337 | 1338 | 40 1339 | 40 1340 | 40 1341 | 1342 | 1343 | 1344 | 1345 | 1346 | 1347 | 255 1348 | 255 1349 | 220 1350 | 1351 | 1352 | 1353 | 1354 | 1355 | 1356 | 0 1357 | 0 1358 | 0 1359 | 1360 | 1361 | 1362 | 1363 | 1364 | 1365 | 1366 | 1367 | 40 1368 | 40 1369 | 40 1370 | 1371 | 1372 | 1373 | 1374 | 1375 | 1376 | 80 1377 | 80 1378 | 80 1379 | 1380 | 1381 | 1382 | 1383 | 1384 | 1385 | 120 1386 | 120 1387 | 120 1388 | 1389 | 1390 | 1391 | 1392 | 1393 | 1394 | 100 1395 | 100 1396 | 100 1397 | 1398 | 1399 | 1400 | 1401 | 1402 | 1403 | 40 1404 | 40 1405 | 40 1406 | 1407 | 1408 | 1409 | 1410 | 1411 | 1412 | 53 1413 | 53 1414 | 53 1415 | 1416 | 1417 | 1418 | 1419 | 1420 | 1421 | 40 1422 | 40 1423 | 40 1424 | 1425 | 1426 | 1427 | 1428 | 1429 | 1430 | 255 1431 | 255 1432 | 255 1433 | 1434 | 1435 | 1436 | 1437 | 1438 | 1439 | 40 1440 | 40 1441 | 40 1442 | 1443 | 1444 | 1445 | 1446 | 1447 | 1448 | 80 1449 | 80 1450 | 80 1451 | 1452 | 1453 | 1454 | 1455 | 1456 | 1457 | 80 1458 | 80 1459 | 80 1460 | 1461 | 1462 | 1463 | 1464 | 1465 | 1466 | 0 1467 | 0 1468 | 0 1469 | 1470 | 1471 | 1472 | 1473 | 1474 | 1475 | 80 1476 | 80 1477 | 80 1478 | 1479 | 1480 | 1481 | 1482 | 1483 | 1484 | 255 1485 | 255 1486 | 220 1487 | 1488 | 1489 | 1490 | 1491 | 1492 | 1493 | 0 1494 | 0 1495 | 0 1496 | 1497 | 1498 | 1499 | 1500 | 1501 | 1502 | 1503 | 1504 | Museo Sans For Dell 1505 | 22 1506 | 50 1507 | false 1508 | 1509 | 1510 | 1511 | Scan folders 1512 | for movies 1513 | 1514 | 1515 | 0 1516 | 1517 | 1518 | 10 1519 | 1520 | 1521 | 1522 | 1523 | 1524 | 1525 | 1526 | 1527 | 1528 | 1529 | 1530 | 0 1531 | 0 1532 | 1533 | 1534 | 1535 | 1536 | 150 1537 | 40 1538 | 1539 | 1540 | 1541 | 1542 | 100 1543 | 40 1544 | 1545 | 1546 | 1547 | 1548 | Museo Sans For Dell 1549 | 10 1550 | 1551 | 1552 | 1553 | Browse 1554 | 1555 | 1556 | 1557 | 1558 | 1559 | 1560 | 1561 | 0 1562 | 0 1563 | 1564 | 1565 | 1566 | 1567 | 700 1568 | 40 1569 | 1570 | 1571 | 1572 | 1573 | 16777215 1574 | 40 1575 | 1576 | 1577 | 1578 | 1579 | Amiri 1580 | 12 1581 | 1582 | 1583 | 1584 | QFrame::WinPanel 1585 | 1586 | 1587 | QFrame::Sunken 1588 | 1589 | 1590 | 2 1591 | 1592 | 1593 | ... 1594 | 1595 | 1596 | 10 1597 | 1598 | 1599 | 1600 | 1601 | 1602 | 1603 | false 1604 | 1605 | 1606 | 1607 | 150 1608 | 40 1609 | 1610 | 1611 | 1612 | 1613 | Museo Sans For Dell 1614 | 10 1615 | 1616 | 1617 | 1618 | Cancel 1619 | 1620 | 1621 | 1622 | 1623 | 1624 | 1625 | 1626 | 0 1627 | 0 1628 | 1629 | 1630 | 1631 | 1632 | 160 1633 | 40 1634 | 1635 | 1636 | 1637 | 1638 | 200 1639 | 40 1640 | 1641 | 1642 | 1643 | 1644 | 1645 | 1646 | 1647 | 1648 | 0 1649 | 255 1650 | 255 1651 | 1652 | 1653 | 1654 | 1655 | 1656 | 1657 | 1658 | 1659 | 0 1660 | 255 1661 | 255 1662 | 1663 | 1664 | 1665 | 1666 | 1667 | 1668 | 1669 | 1670 | 230 1671 | 255 1672 | 255 1673 | 1674 | 1675 | 1676 | 1677 | 1678 | 1679 | 1680 | 1681 | Museo Sans For Dell 1682 | 12 1683 | 50 1684 | false 1685 | 1686 | 1687 | 1688 | Qt::ClickFocus 1689 | 1690 | 1691 | Start scan 1692 | 1693 | 1694 | 1695 | 1696 | 1697 | 1698 | 1699 | 1700 | 0 1701 | 1702 | 1703 | 0 1704 | 1705 | 1706 | 5 1707 | 1708 | 1709 | 1710 | 1711 | 1712 | Museo Sans For Dell 1713 | 12 1714 | 50 1715 | false 1716 | 1717 | 1718 | 1719 | Select subtitle language: 1720 | 1721 | 1722 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 1723 | 1724 | 1725 | 1726 | 1727 | 1728 | 1729 | 1730 | 300 1731 | 16777215 1732 | 1733 | 1734 | 1735 | 1736 | Museo Sans For Dell 1737 | 10 1738 | 1739 | 1740 | 1741 | true 1742 | 1743 | 1744 | 1745 | Albanian 1746 | 1747 | 1748 | 1749 | 1750 | Afghan 1751 | 1752 | 1753 | 1754 | 1755 | New Item 1756 | 1757 | 1758 | 1759 | 1760 | New Item 1761 | 1762 | 1763 | 1764 | 1765 | New Item 1766 | 1767 | 1768 | 1769 | 1770 | New Item 1771 | 1772 | 1773 | 1774 | 1775 | New Item 1776 | 1777 | 1778 | 1779 | 1780 | New Item 1781 | 1782 | 1783 | 1784 | 1785 | New Item 1786 | 1787 | 1788 | 1789 | 1790 | New Item 1791 | 1792 | 1793 | 1794 | 1795 | New Item 1796 | 1797 | 1798 | 1799 | 1800 | New Item 1801 | 1802 | 1803 | 1804 | 1805 | New Item 1806 | 1807 | 1808 | 1809 | 1810 | New Item 1811 | 1812 | 1813 | 1814 | 1815 | New Item 1816 | 1817 | 1818 | 1819 | 1820 | New Item 1821 | 1822 | 1823 | 1824 | 1825 | New Item 1826 | 1827 | 1828 | 1829 | 1830 | New Item 1831 | 1832 | 1833 | 1834 | 1835 | New Item 1836 | 1837 | 1838 | 1839 | 1840 | New Item 1841 | 1842 | 1843 | 1844 | 1845 | New Item 1846 | 1847 | 1848 | 1849 | 1850 | New Item 1851 | 1852 | 1853 | 1854 | 1855 | New Item 1856 | 1857 | 1858 | 1859 | 1860 | New Item 1861 | 1862 | 1863 | 1864 | 1865 | New Item 1866 | 1867 | 1868 | 1869 | 1870 | New Item 1871 | 1872 | 1873 | 1874 | 1875 | New Item 1876 | 1877 | 1878 | 1879 | 1880 | New Item 1881 | 1882 | 1883 | 1884 | 1885 | New Item 1886 | 1887 | 1888 | 1889 | 1890 | New Item 1891 | 1892 | 1893 | 1894 | 1895 | New Item 1896 | 1897 | 1898 | 1899 | 1900 | 1901 | 1902 | 1903 | 1904 | Museo Sans For Dell 1905 | 10 1906 | 1907 | 1908 | 1909 | QFrame::Plain 1910 | 1911 | 1912 | 1 1913 | 1914 | 1915 | 1 1916 | 1917 | 1918 | Language: Albanian 1919 | 1920 | 1921 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 1922 | 1923 | 1924 | 1925 | 1926 | 1927 | 1928 | Qt::Horizontal 1929 | 1930 | 1931 | 1932 | 1933 | 1934 | 1935 | false 1936 | 1937 | 1938 | 1939 | 300 1940 | 40 1941 | 1942 | 1943 | 1944 | 1945 | 350 1946 | 40 1947 | 1948 | 1949 | 1950 | 1951 | Museo Sans For Dell 1952 | 12 1953 | 1954 | 1955 | 1956 | Download 1957 | 1958 | 1959 | false 1960 | 1961 | 1962 | false 1963 | 1964 | 1965 | false 1966 | 1967 | 1968 | false 1969 | 1970 | 1971 | false 1972 | 1973 | 1974 | 1975 | 1976 | 1977 | 1978 | 1979 | 1980 | 5 1981 | 1982 | 1983 | 1984 | 1985 | 1986 | 16777215 1987 | 50 1988 | 1989 | 1990 | 1991 | 1992 | Museo Sans For Dell 1993 | 14 1994 | 1995 | 1996 | 1997 | QFrame::WinPanel 1998 | 1999 | 2000 | QFrame::Sunken 2001 | 2002 | 2003 | 2 2004 | 2005 | 2006 | Ready to download! 2007 | 2008 | 2009 | 5 2010 | 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 0 2018 | 0 2019 | 2020 | 2021 | 2022 | 2023 | 16777215 2024 | 15 2025 | 2026 | 2027 | 2028 | Qt::DefaultContextMenu 2029 | 2030 | 2031 | 0 2032 | 2033 | 2034 | Qt::AlignCenter 2035 | 2036 | 2037 | false 2038 | 2039 | 2040 | QProgressBar::TopToBottom 2041 | 2042 | 2043 | 2044 | 2045 | 2046 | 2047 | 2048 | 2049 | 2050 | 2051 | 2052 | 0 2053 | 0 2054 | 1280 2055 | 26 2056 | 2057 | 2058 | 2059 | 2060 | About 2061 | 2062 | 2063 | 2064 | 2065 | 2066 | 2067 | 2068 | 2069 | 2070 | Scanning 2071 | 2072 | 2073 | 2074 | 2075 | Subtitles 2076 | 2077 | 2078 | 2079 | 2080 | About 2081 | 2082 | 2083 | 2084 | 2085 | Contribute 2086 | 2087 | 2088 | 2089 | 2090 | 2091 | 2092 | -------------------------------------------------------------------------------- /ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukaabra/SubCrawl/85da48f513dceed2ed9f9c60f09914996357e7f9/ui/__init__.py -------------------------------------------------------------------------------- /ui/bindings.py: -------------------------------------------------------------------------------- 1 | from ui.gui import Ui_SubCrawl 2 | from PyQt5 import QtWidgets 3 | from PyQt5.QtCore import pyqtSlot 4 | 5 | import os 6 | import requests 7 | import winsound 8 | import json 9 | import collections 10 | 11 | from scanner import Scanner 12 | from db_interactor import _DBInteractor 13 | from subtitles import SubtitlePreference, SubtitleDownloader 14 | 15 | 16 | class SubCrawl(Ui_SubCrawl, QtWidgets.QMainWindow): 17 | """ 18 | Class which creates all the functions for the GUI to be operational. After the creation it also bind them together. 19 | 20 | """ 21 | 22 | def __init__(self): 23 | Ui_SubCrawl.__init__(self) 24 | QtWidgets.QMainWindow.__init__(self) 25 | self.setupUi(self) 26 | 27 | # TODO: Implement enabling and disabling of buttons depending on the confirmation of selection 28 | self.selection_confirmed = False 29 | self.program_dir = os.getcwd() 30 | self.total_files = 0 31 | 32 | self.subtitle_preference = SubtitlePreference() 33 | self.interactor = _DBInteractor(self.program_dir) 34 | self.interactor.check_if_entries_exist() 35 | self._populate_table() 36 | 37 | self.subtitle_downloader = SubtitleDownloader(self.subtitle_preference, self.PromptLabel, 38 | self.ProgressBar, self.interactor) 39 | 40 | @pyqtSlot() 41 | def _populate_table(self, db_table="all_movies", condition=None): 42 | """ 43 | Goes through media.db database and populates the table if there are any entries in the database upon startup. 44 | 45 | media.db structure: 46 | 47 | all_movies & selected_movies: 48 | id file_name path extension title year rating subtitles sub_language 49 | 50 | :param db_table: (string) table in the database from which to populate 51 | :param condition: (tuple) Tuple with two entries which specify the search condition. Example: 52 | ("id", "12345") The first element is the column and the second is the value. 53 | """ 54 | self.ScannedItems.setRowCount(0) 55 | table_row = self.ScannedItems.rowCount() 56 | 57 | for entry in self.interactor.retrieve(db_table, condition): 58 | self.ScannedItems.insertRow(table_row) 59 | self._set_items_in_table(table_row, entry) 60 | table_row = self.ScannedItems.rowCount() 61 | 62 | # If the "Select All" radio button was checked before the table was populated (table was empty), call the 63 | # function that selects all the movies 64 | if self.SelectAllRadio.isChecked(): 65 | self.select_all_movies(True) 66 | 67 | def _set_items_in_table(self, table_row: int, entry: tuple): 68 | """ 69 | Fills up contents to the cells in that row. 70 | :param table_row: (int) row to fill up 71 | :param entry: (tuple) contents from the database 72 | """ 73 | entry_id, _, entry_location, __, entry_title, entry_year, entry_rating, entry_subs, __ = entry 74 | 75 | self.ScannedItems.setItem(table_row, 0, QtWidgets.QTableWidgetItem(str(entry_id))) 76 | self.ScannedItems.setItem(table_row, 1, QtWidgets.QTableWidgetItem(entry_title)) 77 | self.ScannedItems.setItem(table_row, 2, QtWidgets.QTableWidgetItem(entry_rating)) 78 | self.ScannedItems.setItem(table_row, 3, QtWidgets.QTableWidgetItem(entry_year)) 79 | self.ScannedItems.setItem(table_row, 4, QtWidgets.QTableWidgetItem(entry_location)) 80 | # Boolean values must be written as string values to the GUI table 81 | self.ScannedItems.setItem(table_row, 5, QtWidgets.QTableWidgetItem(entry_subs)) 82 | 83 | def bind_browse_button(self): 84 | """ 85 | Connects the browse button to the method that opens the file dialog. 86 | """ 87 | self.BrowseButton.clicked.connect(self.on_click_browse) 88 | 89 | @pyqtSlot() 90 | # Needed decorator to traverse some compatibility issues with Python and C++ 91 | def on_click_browse(self): 92 | """ 93 | Sets the default directory to the Desktop. Opens up a File Dialog in a mode where the user can only 94 | choose directories. The user chooses the directory which he wishes to scan and the absolute path to that 95 | directory is saved in the "SelectedFolderDisplay" text area. 96 | """ 97 | self.PromptLabel.setText("Browsing...") 98 | # Sets the default directory of the FileDialog to "Desktop" 99 | directory = os.path.join(os.environ["HOMEPATH"], "Desktop") 100 | # Opens the file dialog to select only directories 101 | selected_dir = QtWidgets.QFileDialog.getExistingDirectory(self, "Open a folder", 102 | directory, QtWidgets.QFileDialog.ShowDirsOnly) 103 | # If no directory is chosen to scan, the program sets it back to default (Desktop) 104 | if selected_dir == "": 105 | selected_dir = directory 106 | # Updates the label with the selected folders absolute path 107 | self.SelectedFolderDisplay.setText(selected_dir) 108 | self.PromptLabel.setText("Folder selected") 109 | 110 | def bind_clear_button(self): 111 | """ 112 | Connects the "Clear database" button to the method that deletes all the tables. 113 | """ 114 | self.ClearDBButton.clicked.connect(self.on_click_clear_db) 115 | 116 | @pyqtSlot() 117 | def on_click_clear_db(self): 118 | """ 119 | Deletes all the tables inside the database. 120 | """ 121 | self.interactor.clear_db() 122 | self.interactor.clear_db("selected_movies") 123 | self.interactor.clear_db("search_subs") 124 | self.interactor.clear_db("download_subs") 125 | self.ScannedItems.setRowCount(0) 126 | self.interactor.commit_and_renew_cursor() 127 | self.PromptLabel.setText("Database cleared!") 128 | 129 | def bind_confirm_selection(self): 130 | self.ConfirmSelectionButton.clicked.connect(self.on_click_confirm_selection) 131 | 132 | def on_click_confirm_selection(self): 133 | """ 134 | "Locks" the table (disables selection) and highlights the table to confirm that the selection has been 135 | carried out. 136 | """ 137 | self.ScannedItems.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) 138 | self.CancelSelectionButton.setEnabled(True) 139 | self.ConfirmSelectionButton.setEnabled(False) 140 | self.DownloadButton.setEnabled(True) 141 | self.RemoveEntryButton.setEnabled(False) 142 | 143 | selected_rows = self.ScannedItems.selectionModel().selectedRows() 144 | for row in selected_rows: 145 | condition = ("id", str(row.data())) 146 | # The retrieve method here always returns a single record from the database since there is only one 147 | # record with that ID being passed to it. 148 | self.interactor.copy_to_table("all_movies", "selected_movies", condition) 149 | self.interactor.commit_and_renew_cursor() 150 | 151 | self.ScannedItems.setLineWidth(2) 152 | self.PromptLabel.setText("Selection confirmed!") 153 | 154 | def bind_cancel_selection(self): 155 | self.CancelSelectionButton.clicked.connect(self.on_click_cancel_selection) 156 | 157 | def on_click_cancel_selection(self): 158 | self.ScannedItems.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) 159 | self.CancelSelectionButton.setEnabled(False) 160 | self.ConfirmSelectionButton.setEnabled(True) 161 | self.RemoveEntryButton.setEnabled(False) 162 | self.DownloadButton.setEnabled(False) 163 | self.ScannedItems.setLineWidth(1) 164 | self.interactor.clear_db("selected_movies") 165 | self.PromptLabel.setText("Canceled selection") 166 | 167 | def bind_combo_box(self): 168 | """ 169 | Connects the combo box to the function which changes the text on the label showing the language. 170 | """ 171 | self.LanguageComboBox.activated.connect(self.on_click_language_combo_box) 172 | 173 | def on_click_language_combo_box(self): 174 | """ 175 | Changes the LanguageLabel's text when another language is selected for the subtitles in the combobox. 176 | """ 177 | selected_language = self.LanguageComboBox.currentText() 178 | self.LanguageLabel.setText("Language: {}".format(selected_language)) 179 | self.subtitle_preference.add_language(selected_language) 180 | self.PromptLabel.setText("Subtitle language changed to {}".format(selected_language)) 181 | 182 | def bind_download_button(self): 183 | self.DownloadButton.clicked.connect(self.on_click_download) 184 | 185 | def on_click_download(self): 186 | self.PromptLabel.setText("Commencing download ...") 187 | self.subtitle_downloader.download_from_opensubtitles() 188 | self.on_click_scan() 189 | 190 | if self.SelectAllRadio.isChecked(): 191 | self.SelectAllRadio.setChecked(False) 192 | 193 | def bind_radio_buttons(self): 194 | """ 195 | Connects the "view" radio buttons to the function that repopulates the GUI table. 196 | """ 197 | self.ShowAllRadio.toggled.connect(self.view_radio_buttons) 198 | self.ShowNoSubsRadio.toggled.connect(self.view_radio_buttons) 199 | self.ShowSubsRadio.toggled.connect(self.view_radio_buttons) 200 | 201 | self.SelectAllRadio.toggled.connect(self.select_all_movies) 202 | 203 | @pyqtSlot() 204 | def view_radio_buttons(self): 205 | """ 206 | Changes the items displayed in the table on the GUI depending on the Radio Buttons selected under the label 207 | "Table view:" 208 | """ 209 | # Display all movies from the database 210 | if self.ShowAllRadio.isChecked(): 211 | self.ScannedItems.setRowCount(0) 212 | self._populate_table("all_movies") 213 | 214 | # Display only movies without subtitles 215 | elif self.ShowNoSubsRadio.isChecked(): 216 | self.ScannedItems.setRowCount(0) 217 | self._populate_table("all_movies", ("subtitles", str(False))) 218 | 219 | # Display only movies with subtitles 220 | elif self.ShowSubsRadio.isChecked(): 221 | self.ScannedItems.setRowCount(0) 222 | self._populate_table("all_movies", ("subtitles", str(True))) 223 | 224 | def select_all_movies(self, checked: bool): 225 | """ 226 | Selects all the movies in the GUI table creating a Qt item called SelectionRange. 227 | """ 228 | table_range = QtWidgets.QTableWidgetSelectionRange(0, 0, self.ScannedItems.rowCount() - 1, 229 | self.ScannedItems.columnCount() - 1) 230 | if checked: 231 | self.ScannedItems.setRangeSelected(table_range, True) 232 | else: 233 | self.ScannedItems.setRangeSelected(table_range, False) 234 | 235 | def bind_remove_entry(self): 236 | """ 237 | Button to remove a selected entry in the table 238 | """ 239 | self.RemoveEntryButton.clicked.connect(self.on_click_remove_entry) 240 | 241 | def on_click_remove_entry(self): 242 | """ 243 | Deletes rows from the highest index to the lowest as to avoid bugs while iterating through the rows and 244 | deleting them at the same time. 245 | """ 246 | selected_rows = self.ScannedItems.selectionModel().selectedRows() 247 | rows_to_delete = dict() 248 | for row in selected_rows: 249 | rows_to_delete[row.row()] = (row, row.data()) 250 | 251 | # Creates an OrderedDict which preserves the order of its elements 252 | rows_to_delete = collections.OrderedDict(sorted(rows_to_delete.items(), reverse=True)) 253 | for row in rows_to_delete: 254 | _, row_id = rows_to_delete[row] 255 | condition = ("id", row_id) 256 | self.interactor.delete_entry(condition) 257 | self.ScannedItems.removeRow(row) 258 | 259 | self.interactor.commit_and_renew_cursor() 260 | 261 | def bind_scan_button(self): 262 | """ 263 | Connects the "Start scan" button to the scan method. 264 | """ 265 | self.StartScanButton.clicked.connect(self.on_click_scan) 266 | 267 | @pyqtSlot() 268 | def on_click_scan(self): 269 | """ 270 | A connection method for the "Start scan" button. Clicking the button initiates the scanning of the selected 271 | files, as well as the progress bar animation. The scanning function "perform_scan" is called from 272 | "scanner.py" in which the function for updating the progress bar is located. 273 | """ 274 | # Clear the database and the table in the GUI 275 | self.on_click_clear_db() 276 | # Resets the total amount of files scanned before each scan 277 | self.total_files = 0 278 | self._calculate_total_number_of_files() 279 | progress_tuple = (self.ScanProgressBar.setValue, self.total_files) 280 | 281 | # Initiate the Scanner object with the designated path 282 | scanner = Scanner(self.SelectedFolderDisplay.text(), self.program_dir) 283 | 284 | # Connection error is caught here and not in 'media.py' because it is easier to show an error message and alert 285 | # from 'main.py' 286 | self.PromptLabel.setText("Scanning...") 287 | try: 288 | self._disable_buttons() 289 | scanner.perform_scan(progress_tuple) 290 | duplicate_files = scanner.get_number_of_duplicate_files() 291 | 292 | except requests.ConnectionError: 293 | self._handle_error("There was a connection error! Please check your internet connection.") 294 | 295 | # _get_media_files_extensions not reading the .txt file 296 | except FileNotFoundError: 297 | self._handle_error("Oops! Unfortunately there was a problem!") 298 | 299 | except json.JSONDecodeError: 300 | self._handle_error("There is a problem with the internet connection. Try again later.") 301 | 302 | else: 303 | self.PromptLabel.setText("Folder scanning complete! {} files already exist in the database" 304 | .format(duplicate_files)) 305 | self.view_radio_buttons() 306 | 307 | finally: 308 | self._enable_buttons() 309 | 310 | self.ScanProgressBar.setValue(0) 311 | 312 | def _calculate_total_number_of_files(self): 313 | """ 314 | Calculates the total number of files in the selected folder for the progress bar 315 | """ 316 | for path, dirs, files in os.walk(self.SelectedFolderDisplay.text()): 317 | for _ in files: 318 | self.total_files += 1 319 | 320 | def _handle_error(self, message): 321 | winsound.MessageBeep() 322 | self.PromptLabel.setText(message) 323 | 324 | def bind_table_selection_changed(self): 325 | self.ScannedItems.itemSelectionChanged.connect(self.table_selection_function) 326 | 327 | def table_selection_function(self): 328 | """ 329 | Each time an item is selected in the table, different buttons are enabled or disabled depending on the 330 | number of items selected. In addition, a labels text is also being changed representing the number of 331 | items selected in the GUI table. 332 | """ 333 | selected_rows = self.ScannedItems.selectionModel().selectedRows() 334 | if not selected_rows: 335 | self.ConfirmSelectionButton.setEnabled(False) 336 | self.RemoveEntryButton.setEnabled(False) 337 | else: 338 | self.ConfirmSelectionButton.setEnabled(True) 339 | self.RemoveEntryButton.setEnabled(True) 340 | self.SelectedRowsCount.setText("{} movies selected".format(len(selected_rows))) 341 | 342 | def _disable_buttons(self): 343 | """ 344 | Disables some buttons and enables 1. The list of buttons to disable is below: 345 | 346 | ClearDBButton 347 | DownloadButton 348 | StartScanButton 349 | BrowseButton 350 | 351 | Enabled button: 352 | 353 | CancelButton 354 | """ 355 | self.ClearDBButton.setEnabled(False) 356 | self.DownloadButton.setEnabled(False) 357 | self.StartScanButton.setEnabled(False) 358 | self.BrowseButton.setEnabled(False) 359 | self.CancelButton.setEnabled(True) 360 | 361 | def _enable_buttons(self): 362 | """ 363 | Enables some buttons and disables 1. The list of buttons to enable is below: 364 | 365 | ClearDBButton 366 | DownloadButton 367 | StartScanButton 368 | BrowseButton 369 | 370 | Disable button: 371 | 372 | CancelButton 373 | """ 374 | self.ClearDBButton.setEnabled(True) 375 | self.DownloadButton.setEnabled(False) 376 | self.StartScanButton.setEnabled(True) 377 | self.BrowseButton.setEnabled(True) 378 | self.CancelButton.setEnabled(False) 379 | 380 | def populate_language_combo_box(self): 381 | """ 382 | Clears the default values set in the QtDesigner for the Language Combo box and adds all the languages 383 | from ISO 639-2 contained in a single .json file 384 | """ 385 | self.LanguageComboBox.clear() 386 | with open("resources\\iso 639 2.json", "r") as languages_file: 387 | languages_json = json.load(languages_file) 388 | languages_list = [language["English_Name"] for language in languages_json] 389 | self.LanguageComboBox.addItems(languages_list) 390 | self.LanguageLabel.setText("Language: {}".format(self.LanguageComboBox.itemText(0))) 391 | -------------------------------------------------------------------------------- /ui/gui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'SubCrawl.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.11.3 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt5 import QtCore, QtGui, QtWidgets 10 | 11 | class Ui_SubCrawl(object): 12 | def setupUi(self, SubCrawl): 13 | SubCrawl.setObjectName("SubCrawl") 14 | SubCrawl.resize(1280, 851) 15 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 16 | sizePolicy.setHorizontalStretch(1) 17 | sizePolicy.setVerticalStretch(1) 18 | sizePolicy.setHeightForWidth(SubCrawl.sizePolicy().hasHeightForWidth()) 19 | SubCrawl.setSizePolicy(sizePolicy) 20 | SubCrawl.setMinimumSize(QtCore.QSize(1280, 800)) 21 | SubCrawl.setMaximumSize(QtCore.QSize(1280, 851)) 22 | font = QtGui.QFont() 23 | font.setFamily("Museo Sans For Dell") 24 | font.setPointSize(9) 25 | SubCrawl.setFont(font) 26 | SubCrawl.setLayoutDirection(QtCore.Qt.LeftToRight) 27 | SubCrawl.setDocumentMode(False) 28 | SubCrawl.setTabShape(QtWidgets.QTabWidget.Rounded) 29 | self.centralwidget = QtWidgets.QWidget(SubCrawl) 30 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 31 | sizePolicy.setHorizontalStretch(1) 32 | sizePolicy.setVerticalStretch(1) 33 | sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth()) 34 | self.centralwidget.setSizePolicy(sizePolicy) 35 | self.centralwidget.setMinimumSize(QtCore.QSize(1280, 800)) 36 | self.centralwidget.setMaximumSize(QtCore.QSize(1280, 800)) 37 | self.centralwidget.setObjectName("centralwidget") 38 | self.layoutWidget = QtWidgets.QWidget(self.centralwidget) 39 | self.layoutWidget.setGeometry(QtCore.QRect(0, 0, 1271, 811)) 40 | self.layoutWidget.setObjectName("layoutWidget") 41 | self.gridLayout_7 = QtWidgets.QGridLayout(self.layoutWidget) 42 | self.gridLayout_7.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) 43 | self.gridLayout_7.setContentsMargins(10, 10, 10, 10) 44 | self.gridLayout_7.setSpacing(0) 45 | self.gridLayout_7.setObjectName("gridLayout_7") 46 | self.verticalLayout_2 = QtWidgets.QVBoxLayout() 47 | self.verticalLayout_2.setContentsMargins(-1, 5, -1, 10) 48 | self.verticalLayout_2.setObjectName("verticalLayout_2") 49 | self.ScanProgressBar = QtWidgets.QProgressBar(self.layoutWidget) 50 | self.ScanProgressBar.setMaximumSize(QtCore.QSize(16777215, 15)) 51 | font = QtGui.QFont() 52 | font.setFamily("Amiri") 53 | font.setPointSize(8) 54 | self.ScanProgressBar.setFont(font) 55 | self.ScanProgressBar.setProperty("value", 0) 56 | self.ScanProgressBar.setTextVisible(False) 57 | self.ScanProgressBar.setInvertedAppearance(False) 58 | self.ScanProgressBar.setTextDirection(QtWidgets.QProgressBar.TopToBottom) 59 | self.ScanProgressBar.setObjectName("ScanProgressBar") 60 | self.verticalLayout_2.addWidget(self.ScanProgressBar) 61 | self.line = QtWidgets.QFrame(self.layoutWidget) 62 | font = QtGui.QFont() 63 | font.setPointSize(10) 64 | self.line.setFont(font) 65 | self.line.setFrameShadow(QtWidgets.QFrame.Raised) 66 | self.line.setLineWidth(1) 67 | self.line.setFrameShape(QtWidgets.QFrame.HLine) 68 | self.line.setObjectName("line") 69 | self.verticalLayout_2.addWidget(self.line) 70 | self.ScannedItems = QtWidgets.QTableWidget(self.layoutWidget) 71 | self.ScannedItems.setEnabled(True) 72 | self.ScannedItems.setMinimumSize(QtCore.QSize(1249, 399)) 73 | self.ScannedItems.setMaximumSize(QtCore.QSize(1249, 399)) 74 | font = QtGui.QFont() 75 | font.setFamily("Museo Sans For Dell") 76 | font.setPointSize(10) 77 | font.setBold(False) 78 | font.setWeight(50) 79 | self.ScannedItems.setFont(font) 80 | self.ScannedItems.setFrameShape(QtWidgets.QFrame.Box) 81 | self.ScannedItems.setFrameShadow(QtWidgets.QFrame.Plain) 82 | self.ScannedItems.setLineWidth(1) 83 | self.ScannedItems.setMidLineWidth(1) 84 | self.ScannedItems.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) 85 | self.ScannedItems.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) 86 | self.ScannedItems.setTabKeyNavigation(False) 87 | self.ScannedItems.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) 88 | self.ScannedItems.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) 89 | self.ScannedItems.setTextElideMode(QtCore.Qt.ElideMiddle) 90 | self.ScannedItems.setShowGrid(True) 91 | self.ScannedItems.setGridStyle(QtCore.Qt.SolidLine) 92 | self.ScannedItems.setRowCount(0) 93 | self.ScannedItems.setColumnCount(6) 94 | self.ScannedItems.setObjectName("ScannedItems") 95 | item = QtWidgets.QTableWidgetItem() 96 | item.setTextAlignment(QtCore.Qt.AlignCenter) 97 | font = QtGui.QFont() 98 | font.setFamily("Museo Sans For Dell") 99 | font.setPointSize(12) 100 | font.setBold(True) 101 | font.setWeight(75) 102 | item.setFont(font) 103 | self.ScannedItems.setHorizontalHeaderItem(0, item) 104 | item = QtWidgets.QTableWidgetItem() 105 | item.setTextAlignment(QtCore.Qt.AlignCenter) 106 | font = QtGui.QFont() 107 | font.setFamily("Museo Sans For Dell") 108 | font.setPointSize(12) 109 | font.setBold(True) 110 | font.setWeight(75) 111 | item.setFont(font) 112 | self.ScannedItems.setHorizontalHeaderItem(1, item) 113 | item = QtWidgets.QTableWidgetItem() 114 | item.setTextAlignment(QtCore.Qt.AlignCenter) 115 | font = QtGui.QFont() 116 | font.setFamily("Museo Sans For Dell") 117 | font.setPointSize(12) 118 | font.setBold(True) 119 | font.setWeight(75) 120 | item.setFont(font) 121 | self.ScannedItems.setHorizontalHeaderItem(2, item) 122 | item = QtWidgets.QTableWidgetItem() 123 | item.setTextAlignment(QtCore.Qt.AlignCenter) 124 | font = QtGui.QFont() 125 | font.setFamily("Museo Sans For Dell") 126 | font.setPointSize(12) 127 | font.setBold(True) 128 | font.setWeight(75) 129 | item.setFont(font) 130 | self.ScannedItems.setHorizontalHeaderItem(3, item) 131 | item = QtWidgets.QTableWidgetItem() 132 | item.setTextAlignment(QtCore.Qt.AlignCenter) 133 | font = QtGui.QFont() 134 | font.setFamily("Museo Sans For Dell") 135 | font.setPointSize(12) 136 | font.setBold(True) 137 | font.setWeight(75) 138 | item.setFont(font) 139 | self.ScannedItems.setHorizontalHeaderItem(4, item) 140 | item = QtWidgets.QTableWidgetItem() 141 | item.setTextAlignment(QtCore.Qt.AlignCenter) 142 | font = QtGui.QFont() 143 | font.setFamily("Museo Sans For Dell") 144 | font.setPointSize(12) 145 | font.setBold(True) 146 | font.setWeight(75) 147 | item.setFont(font) 148 | self.ScannedItems.setHorizontalHeaderItem(5, item) 149 | self.ScannedItems.horizontalHeader().setVisible(True) 150 | self.ScannedItems.horizontalHeader().setCascadingSectionResizes(True) 151 | self.ScannedItems.horizontalHeader().setDefaultSectionSize(55) 152 | self.ScannedItems.horizontalHeader().setMinimumSectionSize(208) 153 | self.ScannedItems.horizontalHeader().setSortIndicatorShown(True) 154 | self.ScannedItems.horizontalHeader().setStretchLastSection(False) 155 | self.ScannedItems.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) 156 | self.ScannedItems.verticalHeader().setVisible(False) 157 | self.ScannedItems.verticalHeader().setMinimumSectionSize(37) 158 | self.verticalLayout_2.addWidget(self.ScannedItems) 159 | self.gridLayout_7.addLayout(self.verticalLayout_2, 1, 0, 1, 4) 160 | self.gridLayout_3 = QtWidgets.QGridLayout() 161 | self.gridLayout_3.setContentsMargins(-1, 0, -1, 0) 162 | self.gridLayout_3.setSpacing(5) 163 | self.gridLayout_3.setObjectName("gridLayout_3") 164 | self.frame_2 = QtWidgets.QFrame(self.layoutWidget) 165 | self.frame_2.setObjectName("frame_2") 166 | self.SelectAllRadio = QtWidgets.QRadioButton(self.frame_2) 167 | self.SelectAllRadio.setGeometry(QtCore.QRect(20, 0, 101, 24)) 168 | font = QtGui.QFont() 169 | font.setFamily("Museo Sans For Dell") 170 | font.setPointSize(10) 171 | self.SelectAllRadio.setFont(font) 172 | self.SelectAllRadio.setChecked(False) 173 | self.SelectAllRadio.setObjectName("SelectAllRadio") 174 | self.gridLayout_3.addWidget(self.frame_2, 2, 0, 1, 1) 175 | self.RemoveEntryButton = QtWidgets.QPushButton(self.layoutWidget) 176 | self.RemoveEntryButton.setEnabled(False) 177 | self.RemoveEntryButton.setMinimumSize(QtCore.QSize(150, 30)) 178 | self.RemoveEntryButton.setMaximumSize(QtCore.QSize(150, 30)) 179 | font = QtGui.QFont() 180 | font.setFamily("Museo Sans For Dell") 181 | font.setPointSize(10) 182 | self.RemoveEntryButton.setFont(font) 183 | self.RemoveEntryButton.setObjectName("RemoveEntryButton") 184 | self.gridLayout_3.addWidget(self.RemoveEntryButton, 0, 1, 1, 1) 185 | self.line_6 = QtWidgets.QFrame(self.layoutWidget) 186 | self.line_6.setLineWidth(2) 187 | self.line_6.setFrameShape(QtWidgets.QFrame.HLine) 188 | self.line_6.setFrameShadow(QtWidgets.QFrame.Sunken) 189 | self.line_6.setObjectName("line_6") 190 | self.gridLayout_3.addWidget(self.line_6, 1, 0, 1, 2) 191 | self.ConfirmSelectionButton = QtWidgets.QPushButton(self.layoutWidget) 192 | self.ConfirmSelectionButton.setEnabled(False) 193 | self.ConfirmSelectionButton.setMinimumSize(QtCore.QSize(150, 30)) 194 | self.ConfirmSelectionButton.setMaximumSize(QtCore.QSize(150, 30)) 195 | font = QtGui.QFont() 196 | font.setFamily("Museo Sans For Dell") 197 | font.setPointSize(10) 198 | self.ConfirmSelectionButton.setFont(font) 199 | self.ConfirmSelectionButton.setCheckable(False) 200 | self.ConfirmSelectionButton.setChecked(False) 201 | self.ConfirmSelectionButton.setAutoDefault(False) 202 | self.ConfirmSelectionButton.setDefault(False) 203 | self.ConfirmSelectionButton.setFlat(False) 204 | self.ConfirmSelectionButton.setObjectName("ConfirmSelectionButton") 205 | self.gridLayout_3.addWidget(self.ConfirmSelectionButton, 3, 0, 1, 1) 206 | self.ClearDBButton = QtWidgets.QPushButton(self.layoutWidget) 207 | self.ClearDBButton.setEnabled(True) 208 | self.ClearDBButton.setMinimumSize(QtCore.QSize(150, 30)) 209 | self.ClearDBButton.setMaximumSize(QtCore.QSize(150, 30)) 210 | font = QtGui.QFont() 211 | font.setFamily("Museo Sans For Dell") 212 | font.setPointSize(10) 213 | self.ClearDBButton.setFont(font) 214 | self.ClearDBButton.setObjectName("ClearDBButton") 215 | self.gridLayout_3.addWidget(self.ClearDBButton, 0, 0, 1, 1) 216 | self.CancelSelectionButton = QtWidgets.QPushButton(self.layoutWidget) 217 | self.CancelSelectionButton.setEnabled(False) 218 | self.CancelSelectionButton.setMinimumSize(QtCore.QSize(150, 30)) 219 | self.CancelSelectionButton.setMaximumSize(QtCore.QSize(150, 30)) 220 | font = QtGui.QFont() 221 | font.setFamily("Museo Sans For Dell") 222 | font.setPointSize(10) 223 | self.CancelSelectionButton.setFont(font) 224 | self.CancelSelectionButton.setObjectName("CancelSelectionButton") 225 | self.gridLayout_3.addWidget(self.CancelSelectionButton, 3, 1, 1, 1) 226 | self.SelectedRowsCount = QtWidgets.QLabel(self.layoutWidget) 227 | font = QtGui.QFont() 228 | font.setFamily("Museo Sans For Dell") 229 | font.setPointSize(10) 230 | self.SelectedRowsCount.setFont(font) 231 | self.SelectedRowsCount.setText("") 232 | self.SelectedRowsCount.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) 233 | self.SelectedRowsCount.setObjectName("SelectedRowsCount") 234 | self.gridLayout_3.addWidget(self.SelectedRowsCount, 2, 1, 1, 1) 235 | self.gridLayout_7.addLayout(self.gridLayout_3, 2, 1, 1, 2) 236 | self.gridLayout = QtWidgets.QGridLayout() 237 | self.gridLayout.setObjectName("gridLayout") 238 | self.label_3 = QtWidgets.QLabel(self.layoutWidget) 239 | font = QtGui.QFont() 240 | font.setFamily("Museo Sans For Dell") 241 | font.setPointSize(12) 242 | font.setBold(False) 243 | font.setWeight(50) 244 | self.label_3.setFont(font) 245 | self.label_3.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) 246 | self.label_3.setObjectName("label_3") 247 | self.gridLayout.addWidget(self.label_3, 0, 0, 1, 1) 248 | self.ViewButtonFrame = QtWidgets.QFrame(self.layoutWidget) 249 | self.ViewButtonFrame.setObjectName("ViewButtonFrame") 250 | self.ShowAllRadio = QtWidgets.QRadioButton(self.ViewButtonFrame) 251 | self.ShowAllRadio.setGeometry(QtCore.QRect(6, 12, 154, 24)) 252 | font = QtGui.QFont() 253 | font.setFamily("Museo Sans For Dell") 254 | font.setPointSize(10) 255 | self.ShowAllRadio.setFont(font) 256 | self.ShowAllRadio.setChecked(True) 257 | self.ShowAllRadio.setObjectName("ShowAllRadio") 258 | self.ShowSubsRadio = QtWidgets.QRadioButton(self.ViewButtonFrame) 259 | self.ShowSubsRadio.setGeometry(QtCore.QRect(6, 43, 241, 24)) 260 | font = QtGui.QFont() 261 | font.setFamily("Museo Sans For Dell") 262 | font.setPointSize(10) 263 | self.ShowSubsRadio.setFont(font) 264 | self.ShowSubsRadio.setObjectName("ShowSubsRadio") 265 | self.ShowNoSubsRadio = QtWidgets.QRadioButton(self.ViewButtonFrame) 266 | self.ShowNoSubsRadio.setGeometry(QtCore.QRect(6, 74, 268, 24)) 267 | font = QtGui.QFont() 268 | font.setFamily("Museo Sans For Dell") 269 | font.setPointSize(10) 270 | self.ShowNoSubsRadio.setFont(font) 271 | self.ShowNoSubsRadio.setObjectName("ShowNoSubsRadio") 272 | self.gridLayout.addWidget(self.ViewButtonFrame, 1, 0, 1, 1) 273 | self.gridLayout.setRowStretch(1, 1) 274 | self.gridLayout_7.addLayout(self.gridLayout, 2, 0, 1, 1) 275 | self.horizontalLayout = QtWidgets.QHBoxLayout() 276 | self.horizontalLayout.setObjectName("horizontalLayout") 277 | self.ScanLabel_2 = QtWidgets.QLabel(self.layoutWidget) 278 | self.ScanLabel_2.setMinimumSize(QtCore.QSize(290, 110)) 279 | palette = QtGui.QPalette() 280 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 281 | brush.setStyle(QtCore.Qt.SolidPattern) 282 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush) 283 | brush = QtGui.QBrush(QtGui.QColor(80, 80, 80)) 284 | brush.setStyle(QtCore.Qt.SolidPattern) 285 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush) 286 | brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) 287 | brush.setStyle(QtCore.Qt.SolidPattern) 288 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Light, brush) 289 | brush = QtGui.QBrush(QtGui.QColor(100, 100, 100)) 290 | brush.setStyle(QtCore.Qt.SolidPattern) 291 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Midlight, brush) 292 | brush = QtGui.QBrush(QtGui.QColor(40, 40, 40)) 293 | brush.setStyle(QtCore.Qt.SolidPattern) 294 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Dark, brush) 295 | brush = QtGui.QBrush(QtGui.QColor(53, 53, 53)) 296 | brush.setStyle(QtCore.Qt.SolidPattern) 297 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Mid, brush) 298 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 299 | brush.setStyle(QtCore.Qt.SolidPattern) 300 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Text, brush) 301 | brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) 302 | brush.setStyle(QtCore.Qt.SolidPattern) 303 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.BrightText, brush) 304 | brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) 305 | brush.setStyle(QtCore.Qt.SolidPattern) 306 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ButtonText, brush) 307 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 308 | brush.setStyle(QtCore.Qt.SolidPattern) 309 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush) 310 | brush = QtGui.QBrush(QtGui.QColor(80, 80, 80)) 311 | brush.setStyle(QtCore.Qt.SolidPattern) 312 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush) 313 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 314 | brush.setStyle(QtCore.Qt.SolidPattern) 315 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Shadow, brush) 316 | brush = QtGui.QBrush(QtGui.QColor(40, 40, 40)) 317 | brush.setStyle(QtCore.Qt.SolidPattern) 318 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.AlternateBase, brush) 319 | brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) 320 | brush.setStyle(QtCore.Qt.SolidPattern) 321 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipBase, brush) 322 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 323 | brush.setStyle(QtCore.Qt.SolidPattern) 324 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipText, brush) 325 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 326 | brush.setStyle(QtCore.Qt.SolidPattern) 327 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush) 328 | brush = QtGui.QBrush(QtGui.QColor(80, 80, 80)) 329 | brush.setStyle(QtCore.Qt.SolidPattern) 330 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush) 331 | brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) 332 | brush.setStyle(QtCore.Qt.SolidPattern) 333 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Light, brush) 334 | brush = QtGui.QBrush(QtGui.QColor(100, 100, 100)) 335 | brush.setStyle(QtCore.Qt.SolidPattern) 336 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Midlight, brush) 337 | brush = QtGui.QBrush(QtGui.QColor(40, 40, 40)) 338 | brush.setStyle(QtCore.Qt.SolidPattern) 339 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Dark, brush) 340 | brush = QtGui.QBrush(QtGui.QColor(53, 53, 53)) 341 | brush.setStyle(QtCore.Qt.SolidPattern) 342 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Mid, brush) 343 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 344 | brush.setStyle(QtCore.Qt.SolidPattern) 345 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Text, brush) 346 | brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) 347 | brush.setStyle(QtCore.Qt.SolidPattern) 348 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.BrightText, brush) 349 | brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) 350 | brush.setStyle(QtCore.Qt.SolidPattern) 351 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ButtonText, brush) 352 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 353 | brush.setStyle(QtCore.Qt.SolidPattern) 354 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush) 355 | brush = QtGui.QBrush(QtGui.QColor(80, 80, 80)) 356 | brush.setStyle(QtCore.Qt.SolidPattern) 357 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush) 358 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 359 | brush.setStyle(QtCore.Qt.SolidPattern) 360 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Shadow, brush) 361 | brush = QtGui.QBrush(QtGui.QColor(40, 40, 40)) 362 | brush.setStyle(QtCore.Qt.SolidPattern) 363 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.AlternateBase, brush) 364 | brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) 365 | brush.setStyle(QtCore.Qt.SolidPattern) 366 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipBase, brush) 367 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 368 | brush.setStyle(QtCore.Qt.SolidPattern) 369 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipText, brush) 370 | brush = QtGui.QBrush(QtGui.QColor(40, 40, 40)) 371 | brush.setStyle(QtCore.Qt.SolidPattern) 372 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush) 373 | brush = QtGui.QBrush(QtGui.QColor(80, 80, 80)) 374 | brush.setStyle(QtCore.Qt.SolidPattern) 375 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush) 376 | brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) 377 | brush.setStyle(QtCore.Qt.SolidPattern) 378 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Light, brush) 379 | brush = QtGui.QBrush(QtGui.QColor(100, 100, 100)) 380 | brush.setStyle(QtCore.Qt.SolidPattern) 381 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Midlight, brush) 382 | brush = QtGui.QBrush(QtGui.QColor(40, 40, 40)) 383 | brush.setStyle(QtCore.Qt.SolidPattern) 384 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Dark, brush) 385 | brush = QtGui.QBrush(QtGui.QColor(53, 53, 53)) 386 | brush.setStyle(QtCore.Qt.SolidPattern) 387 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Mid, brush) 388 | brush = QtGui.QBrush(QtGui.QColor(40, 40, 40)) 389 | brush.setStyle(QtCore.Qt.SolidPattern) 390 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Text, brush) 391 | brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) 392 | brush.setStyle(QtCore.Qt.SolidPattern) 393 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.BrightText, brush) 394 | brush = QtGui.QBrush(QtGui.QColor(40, 40, 40)) 395 | brush.setStyle(QtCore.Qt.SolidPattern) 396 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, brush) 397 | brush = QtGui.QBrush(QtGui.QColor(80, 80, 80)) 398 | brush.setStyle(QtCore.Qt.SolidPattern) 399 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush) 400 | brush = QtGui.QBrush(QtGui.QColor(80, 80, 80)) 401 | brush.setStyle(QtCore.Qt.SolidPattern) 402 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush) 403 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 404 | brush.setStyle(QtCore.Qt.SolidPattern) 405 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Shadow, brush) 406 | brush = QtGui.QBrush(QtGui.QColor(80, 80, 80)) 407 | brush.setStyle(QtCore.Qt.SolidPattern) 408 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.AlternateBase, brush) 409 | brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) 410 | brush.setStyle(QtCore.Qt.SolidPattern) 411 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipBase, brush) 412 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 413 | brush.setStyle(QtCore.Qt.SolidPattern) 414 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipText, brush) 415 | self.ScanLabel_2.setPalette(palette) 416 | font = QtGui.QFont() 417 | font.setFamily("Museo Sans For Dell") 418 | font.setPointSize(46) 419 | font.setBold(False) 420 | font.setItalic(False) 421 | font.setWeight(50) 422 | self.ScanLabel_2.setFont(font) 423 | self.ScanLabel_2.setObjectName("ScanLabel_2") 424 | self.horizontalLayout.addWidget(self.ScanLabel_2) 425 | self.line_4 = QtWidgets.QFrame(self.layoutWidget) 426 | font = QtGui.QFont() 427 | font.setPointSize(12) 428 | self.line_4.setFont(font) 429 | self.line_4.setLineWidth(1) 430 | self.line_4.setFrameShape(QtWidgets.QFrame.VLine) 431 | self.line_4.setFrameShadow(QtWidgets.QFrame.Sunken) 432 | self.line_4.setObjectName("line_4") 433 | self.horizontalLayout.addWidget(self.line_4) 434 | self.ScanLabel = QtWidgets.QLabel(self.layoutWidget) 435 | self.ScanLabel.setMinimumSize(QtCore.QSize(250, 110)) 436 | palette = QtGui.QPalette() 437 | brush = QtGui.QBrush(QtGui.QColor(102, 102, 102)) 438 | brush.setStyle(QtCore.Qt.SolidPattern) 439 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush) 440 | brush = QtGui.QBrush(QtGui.QColor(80, 80, 80)) 441 | brush.setStyle(QtCore.Qt.SolidPattern) 442 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush) 443 | brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) 444 | brush.setStyle(QtCore.Qt.SolidPattern) 445 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Light, brush) 446 | brush = QtGui.QBrush(QtGui.QColor(100, 100, 100)) 447 | brush.setStyle(QtCore.Qt.SolidPattern) 448 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Midlight, brush) 449 | brush = QtGui.QBrush(QtGui.QColor(40, 40, 40)) 450 | brush.setStyle(QtCore.Qt.SolidPattern) 451 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Dark, brush) 452 | brush = QtGui.QBrush(QtGui.QColor(53, 53, 53)) 453 | brush.setStyle(QtCore.Qt.SolidPattern) 454 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Mid, brush) 455 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 456 | brush.setStyle(QtCore.Qt.SolidPattern) 457 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Text, brush) 458 | brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) 459 | brush.setStyle(QtCore.Qt.SolidPattern) 460 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.BrightText, brush) 461 | brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) 462 | brush.setStyle(QtCore.Qt.SolidPattern) 463 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ButtonText, brush) 464 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 465 | brush.setStyle(QtCore.Qt.SolidPattern) 466 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush) 467 | brush = QtGui.QBrush(QtGui.QColor(80, 80, 80)) 468 | brush.setStyle(QtCore.Qt.SolidPattern) 469 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush) 470 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 471 | brush.setStyle(QtCore.Qt.SolidPattern) 472 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Shadow, brush) 473 | brush = QtGui.QBrush(QtGui.QColor(40, 40, 40)) 474 | brush.setStyle(QtCore.Qt.SolidPattern) 475 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.AlternateBase, brush) 476 | brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) 477 | brush.setStyle(QtCore.Qt.SolidPattern) 478 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipBase, brush) 479 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 480 | brush.setStyle(QtCore.Qt.SolidPattern) 481 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipText, brush) 482 | brush = QtGui.QBrush(QtGui.QColor(102, 102, 102)) 483 | brush.setStyle(QtCore.Qt.SolidPattern) 484 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush) 485 | brush = QtGui.QBrush(QtGui.QColor(80, 80, 80)) 486 | brush.setStyle(QtCore.Qt.SolidPattern) 487 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush) 488 | brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) 489 | brush.setStyle(QtCore.Qt.SolidPattern) 490 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Light, brush) 491 | brush = QtGui.QBrush(QtGui.QColor(100, 100, 100)) 492 | brush.setStyle(QtCore.Qt.SolidPattern) 493 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Midlight, brush) 494 | brush = QtGui.QBrush(QtGui.QColor(40, 40, 40)) 495 | brush.setStyle(QtCore.Qt.SolidPattern) 496 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Dark, brush) 497 | brush = QtGui.QBrush(QtGui.QColor(53, 53, 53)) 498 | brush.setStyle(QtCore.Qt.SolidPattern) 499 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Mid, brush) 500 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 501 | brush.setStyle(QtCore.Qt.SolidPattern) 502 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Text, brush) 503 | brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) 504 | brush.setStyle(QtCore.Qt.SolidPattern) 505 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.BrightText, brush) 506 | brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) 507 | brush.setStyle(QtCore.Qt.SolidPattern) 508 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ButtonText, brush) 509 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 510 | brush.setStyle(QtCore.Qt.SolidPattern) 511 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush) 512 | brush = QtGui.QBrush(QtGui.QColor(80, 80, 80)) 513 | brush.setStyle(QtCore.Qt.SolidPattern) 514 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush) 515 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 516 | brush.setStyle(QtCore.Qt.SolidPattern) 517 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Shadow, brush) 518 | brush = QtGui.QBrush(QtGui.QColor(40, 40, 40)) 519 | brush.setStyle(QtCore.Qt.SolidPattern) 520 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.AlternateBase, brush) 521 | brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) 522 | brush.setStyle(QtCore.Qt.SolidPattern) 523 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipBase, brush) 524 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 525 | brush.setStyle(QtCore.Qt.SolidPattern) 526 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipText, brush) 527 | brush = QtGui.QBrush(QtGui.QColor(40, 40, 40)) 528 | brush.setStyle(QtCore.Qt.SolidPattern) 529 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush) 530 | brush = QtGui.QBrush(QtGui.QColor(80, 80, 80)) 531 | brush.setStyle(QtCore.Qt.SolidPattern) 532 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush) 533 | brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) 534 | brush.setStyle(QtCore.Qt.SolidPattern) 535 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Light, brush) 536 | brush = QtGui.QBrush(QtGui.QColor(100, 100, 100)) 537 | brush.setStyle(QtCore.Qt.SolidPattern) 538 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Midlight, brush) 539 | brush = QtGui.QBrush(QtGui.QColor(40, 40, 40)) 540 | brush.setStyle(QtCore.Qt.SolidPattern) 541 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Dark, brush) 542 | brush = QtGui.QBrush(QtGui.QColor(53, 53, 53)) 543 | brush.setStyle(QtCore.Qt.SolidPattern) 544 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Mid, brush) 545 | brush = QtGui.QBrush(QtGui.QColor(40, 40, 40)) 546 | brush.setStyle(QtCore.Qt.SolidPattern) 547 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Text, brush) 548 | brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) 549 | brush.setStyle(QtCore.Qt.SolidPattern) 550 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.BrightText, brush) 551 | brush = QtGui.QBrush(QtGui.QColor(40, 40, 40)) 552 | brush.setStyle(QtCore.Qt.SolidPattern) 553 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, brush) 554 | brush = QtGui.QBrush(QtGui.QColor(80, 80, 80)) 555 | brush.setStyle(QtCore.Qt.SolidPattern) 556 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush) 557 | brush = QtGui.QBrush(QtGui.QColor(80, 80, 80)) 558 | brush.setStyle(QtCore.Qt.SolidPattern) 559 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush) 560 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 561 | brush.setStyle(QtCore.Qt.SolidPattern) 562 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Shadow, brush) 563 | brush = QtGui.QBrush(QtGui.QColor(80, 80, 80)) 564 | brush.setStyle(QtCore.Qt.SolidPattern) 565 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.AlternateBase, brush) 566 | brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) 567 | brush.setStyle(QtCore.Qt.SolidPattern) 568 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipBase, brush) 569 | brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) 570 | brush.setStyle(QtCore.Qt.SolidPattern) 571 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipText, brush) 572 | self.ScanLabel.setPalette(palette) 573 | font = QtGui.QFont() 574 | font.setFamily("Museo Sans For Dell") 575 | font.setPointSize(22) 576 | font.setBold(False) 577 | font.setWeight(50) 578 | self.ScanLabel.setFont(font) 579 | self.ScanLabel.setIndent(10) 580 | self.ScanLabel.setObjectName("ScanLabel") 581 | self.horizontalLayout.addWidget(self.ScanLabel) 582 | self.gridLayout_7.addLayout(self.horizontalLayout, 0, 0, 1, 2) 583 | self.gridLayout_5 = QtWidgets.QGridLayout() 584 | self.gridLayout_5.setObjectName("gridLayout_5") 585 | self.BrowseButton = QtWidgets.QPushButton(self.layoutWidget) 586 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed) 587 | sizePolicy.setHorizontalStretch(0) 588 | sizePolicy.setVerticalStretch(0) 589 | sizePolicy.setHeightForWidth(self.BrowseButton.sizePolicy().hasHeightForWidth()) 590 | self.BrowseButton.setSizePolicy(sizePolicy) 591 | self.BrowseButton.setMinimumSize(QtCore.QSize(150, 40)) 592 | self.BrowseButton.setMaximumSize(QtCore.QSize(100, 40)) 593 | font = QtGui.QFont() 594 | font.setFamily("Museo Sans For Dell") 595 | font.setPointSize(10) 596 | self.BrowseButton.setFont(font) 597 | self.BrowseButton.setObjectName("BrowseButton") 598 | self.gridLayout_5.addWidget(self.BrowseButton, 0, 0, 1, 1) 599 | self.SelectedFolderDisplay = QtWidgets.QLabel(self.layoutWidget) 600 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) 601 | sizePolicy.setHorizontalStretch(0) 602 | sizePolicy.setVerticalStretch(0) 603 | sizePolicy.setHeightForWidth(self.SelectedFolderDisplay.sizePolicy().hasHeightForWidth()) 604 | self.SelectedFolderDisplay.setSizePolicy(sizePolicy) 605 | self.SelectedFolderDisplay.setMinimumSize(QtCore.QSize(700, 40)) 606 | self.SelectedFolderDisplay.setMaximumSize(QtCore.QSize(16777215, 40)) 607 | font = QtGui.QFont() 608 | font.setFamily("Amiri") 609 | font.setPointSize(12) 610 | self.SelectedFolderDisplay.setFont(font) 611 | self.SelectedFolderDisplay.setFrameShape(QtWidgets.QFrame.WinPanel) 612 | self.SelectedFolderDisplay.setFrameShadow(QtWidgets.QFrame.Sunken) 613 | self.SelectedFolderDisplay.setLineWidth(2) 614 | self.SelectedFolderDisplay.setIndent(10) 615 | self.SelectedFolderDisplay.setObjectName("SelectedFolderDisplay") 616 | self.gridLayout_5.addWidget(self.SelectedFolderDisplay, 0, 1, 1, 2) 617 | self.CancelButton = QtWidgets.QPushButton(self.layoutWidget) 618 | self.CancelButton.setEnabled(False) 619 | self.CancelButton.setMaximumSize(QtCore.QSize(150, 40)) 620 | font = QtGui.QFont() 621 | font.setFamily("Museo Sans For Dell") 622 | font.setPointSize(10) 623 | self.CancelButton.setFont(font) 624 | self.CancelButton.setObjectName("CancelButton") 625 | self.gridLayout_5.addWidget(self.CancelButton, 1, 1, 1, 1) 626 | self.StartScanButton = QtWidgets.QPushButton(self.layoutWidget) 627 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Expanding) 628 | sizePolicy.setHorizontalStretch(0) 629 | sizePolicy.setVerticalStretch(0) 630 | sizePolicy.setHeightForWidth(self.StartScanButton.sizePolicy().hasHeightForWidth()) 631 | self.StartScanButton.setSizePolicy(sizePolicy) 632 | self.StartScanButton.setMinimumSize(QtCore.QSize(160, 40)) 633 | self.StartScanButton.setMaximumSize(QtCore.QSize(200, 40)) 634 | palette = QtGui.QPalette() 635 | brush = QtGui.QBrush(QtGui.QColor(0, 255, 255)) 636 | brush.setStyle(QtCore.Qt.SolidPattern) 637 | palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush) 638 | brush = QtGui.QBrush(QtGui.QColor(0, 255, 255)) 639 | brush.setStyle(QtCore.Qt.SolidPattern) 640 | palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush) 641 | brush = QtGui.QBrush(QtGui.QColor(230, 255, 255)) 642 | brush.setStyle(QtCore.Qt.SolidPattern) 643 | palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush) 644 | self.StartScanButton.setPalette(palette) 645 | font = QtGui.QFont() 646 | font.setFamily("Museo Sans For Dell") 647 | font.setPointSize(12) 648 | font.setBold(False) 649 | font.setWeight(50) 650 | self.StartScanButton.setFont(font) 651 | self.StartScanButton.setFocusPolicy(QtCore.Qt.ClickFocus) 652 | self.StartScanButton.setObjectName("StartScanButton") 653 | self.gridLayout_5.addWidget(self.StartScanButton, 1, 2, 1, 1) 654 | self.gridLayout_7.addLayout(self.gridLayout_5, 0, 2, 1, 2) 655 | self.gridLayout_4 = QtWidgets.QGridLayout() 656 | self.gridLayout_4.setContentsMargins(-1, 0, -1, 0) 657 | self.gridLayout_4.setSpacing(5) 658 | self.gridLayout_4.setObjectName("gridLayout_4") 659 | self.label = QtWidgets.QLabel(self.layoutWidget) 660 | font = QtGui.QFont() 661 | font.setFamily("Museo Sans For Dell") 662 | font.setPointSize(12) 663 | font.setBold(False) 664 | font.setWeight(50) 665 | self.label.setFont(font) 666 | self.label.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) 667 | self.label.setObjectName("label") 668 | self.gridLayout_4.addWidget(self.label, 0, 0, 1, 1) 669 | self.LanguageComboBox = QtWidgets.QComboBox(self.layoutWidget) 670 | self.LanguageComboBox.setMaximumSize(QtCore.QSize(300, 16777215)) 671 | font = QtGui.QFont() 672 | font.setFamily("Museo Sans For Dell") 673 | font.setPointSize(10) 674 | self.LanguageComboBox.setFont(font) 675 | self.LanguageComboBox.setFrame(True) 676 | self.LanguageComboBox.setObjectName("LanguageComboBox") 677 | self.LanguageComboBox.addItem("") 678 | self.LanguageComboBox.addItem("") 679 | self.LanguageComboBox.addItem("") 680 | self.LanguageComboBox.addItem("") 681 | self.LanguageComboBox.addItem("") 682 | self.LanguageComboBox.addItem("") 683 | self.LanguageComboBox.addItem("") 684 | self.LanguageComboBox.addItem("") 685 | self.LanguageComboBox.addItem("") 686 | self.LanguageComboBox.addItem("") 687 | self.LanguageComboBox.addItem("") 688 | self.LanguageComboBox.addItem("") 689 | self.LanguageComboBox.addItem("") 690 | self.LanguageComboBox.addItem("") 691 | self.LanguageComboBox.addItem("") 692 | self.LanguageComboBox.addItem("") 693 | self.LanguageComboBox.addItem("") 694 | self.LanguageComboBox.addItem("") 695 | self.LanguageComboBox.addItem("") 696 | self.LanguageComboBox.addItem("") 697 | self.LanguageComboBox.addItem("") 698 | self.LanguageComboBox.addItem("") 699 | self.LanguageComboBox.addItem("") 700 | self.LanguageComboBox.addItem("") 701 | self.LanguageComboBox.addItem("") 702 | self.LanguageComboBox.addItem("") 703 | self.LanguageComboBox.addItem("") 704 | self.LanguageComboBox.addItem("") 705 | self.LanguageComboBox.addItem("") 706 | self.LanguageComboBox.addItem("") 707 | self.LanguageComboBox.addItem("") 708 | self.gridLayout_4.addWidget(self.LanguageComboBox, 1, 0, 1, 1) 709 | self.LanguageLabel = QtWidgets.QLabel(self.layoutWidget) 710 | font = QtGui.QFont() 711 | font.setFamily("Museo Sans For Dell") 712 | font.setPointSize(10) 713 | self.LanguageLabel.setFont(font) 714 | self.LanguageLabel.setFrameShadow(QtWidgets.QFrame.Plain) 715 | self.LanguageLabel.setLineWidth(1) 716 | self.LanguageLabel.setMidLineWidth(1) 717 | self.LanguageLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) 718 | self.LanguageLabel.setObjectName("LanguageLabel") 719 | self.gridLayout_4.addWidget(self.LanguageLabel, 2, 0, 1, 1) 720 | self.line_2 = QtWidgets.QFrame(self.layoutWidget) 721 | self.line_2.setFrameShape(QtWidgets.QFrame.HLine) 722 | self.line_2.setFrameShadow(QtWidgets.QFrame.Sunken) 723 | self.line_2.setObjectName("line_2") 724 | self.gridLayout_4.addWidget(self.line_2, 3, 0, 1, 1) 725 | self.DownloadButton = QtWidgets.QPushButton(self.layoutWidget) 726 | self.DownloadButton.setEnabled(False) 727 | self.DownloadButton.setMinimumSize(QtCore.QSize(300, 40)) 728 | self.DownloadButton.setMaximumSize(QtCore.QSize(350, 40)) 729 | font = QtGui.QFont() 730 | font.setFamily("Museo Sans For Dell") 731 | font.setPointSize(12) 732 | self.DownloadButton.setFont(font) 733 | self.DownloadButton.setCheckable(False) 734 | self.DownloadButton.setChecked(False) 735 | self.DownloadButton.setAutoDefault(False) 736 | self.DownloadButton.setDefault(False) 737 | self.DownloadButton.setFlat(False) 738 | self.DownloadButton.setObjectName("DownloadButton") 739 | self.gridLayout_4.addWidget(self.DownloadButton, 4, 0, 1, 1) 740 | self.gridLayout_7.addLayout(self.gridLayout_4, 2, 3, 1, 1) 741 | self.verticalLayout = QtWidgets.QVBoxLayout() 742 | self.verticalLayout.setContentsMargins(-1, 5, -1, -1) 743 | self.verticalLayout.setObjectName("verticalLayout") 744 | self.PromptLabel = QtWidgets.QLabel(self.layoutWidget) 745 | self.PromptLabel.setMaximumSize(QtCore.QSize(16777215, 50)) 746 | font = QtGui.QFont() 747 | font.setFamily("Museo Sans For Dell") 748 | font.setPointSize(14) 749 | self.PromptLabel.setFont(font) 750 | self.PromptLabel.setFrameShape(QtWidgets.QFrame.WinPanel) 751 | self.PromptLabel.setFrameShadow(QtWidgets.QFrame.Sunken) 752 | self.PromptLabel.setLineWidth(2) 753 | self.PromptLabel.setIndent(5) 754 | self.PromptLabel.setObjectName("PromptLabel") 755 | self.verticalLayout.addWidget(self.PromptLabel) 756 | self.ProgressBar = QtWidgets.QProgressBar(self.layoutWidget) 757 | self.ProgressBar.setMinimumSize(QtCore.QSize(0, 0)) 758 | self.ProgressBar.setMaximumSize(QtCore.QSize(16777215, 15)) 759 | self.ProgressBar.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu) 760 | self.ProgressBar.setProperty("value", 0) 761 | self.ProgressBar.setAlignment(QtCore.Qt.AlignCenter) 762 | self.ProgressBar.setTextVisible(False) 763 | self.ProgressBar.setTextDirection(QtWidgets.QProgressBar.TopToBottom) 764 | self.ProgressBar.setObjectName("ProgressBar") 765 | self.verticalLayout.addWidget(self.ProgressBar) 766 | self.gridLayout_7.addLayout(self.verticalLayout, 3, 0, 1, 4) 767 | SubCrawl.setCentralWidget(self.centralwidget) 768 | self.menubar = QtWidgets.QMenuBar(SubCrawl) 769 | self.menubar.setGeometry(QtCore.QRect(0, 0, 1280, 26)) 770 | self.menubar.setObjectName("menubar") 771 | self.menuAbout = QtWidgets.QMenu(self.menubar) 772 | self.menuAbout.setObjectName("menuAbout") 773 | SubCrawl.setMenuBar(self.menubar) 774 | self.statusbar = QtWidgets.QStatusBar(SubCrawl) 775 | self.statusbar.setObjectName("statusbar") 776 | SubCrawl.setStatusBar(self.statusbar) 777 | self.actionScanning = QtWidgets.QAction(SubCrawl) 778 | self.actionScanning.setObjectName("actionScanning") 779 | self.actionSubtitles = QtWidgets.QAction(SubCrawl) 780 | self.actionSubtitles.setObjectName("actionSubtitles") 781 | self.actionAbout = QtWidgets.QAction(SubCrawl) 782 | self.actionAbout.setObjectName("actionAbout") 783 | self.actionContribute = QtWidgets.QAction(SubCrawl) 784 | self.actionContribute.setObjectName("actionContribute") 785 | self.menuAbout.addAction(self.actionAbout) 786 | self.menuAbout.addAction(self.actionContribute) 787 | self.menubar.addAction(self.menuAbout.menuAction()) 788 | 789 | self.retranslateUi(SubCrawl) 790 | QtCore.QMetaObject.connectSlotsByName(SubCrawl) 791 | 792 | def retranslateUi(self, SubCrawl): 793 | _translate = QtCore.QCoreApplication.translate 794 | SubCrawl.setWindowTitle(_translate("SubCrawl", "SubCrawl v0.9")) 795 | self.ScannedItems.setSortingEnabled(True) 796 | item = self.ScannedItems.horizontalHeaderItem(0) 797 | item.setText(_translate("SubCrawl", "IMDb ID")) 798 | item = self.ScannedItems.horizontalHeaderItem(1) 799 | item.setText(_translate("SubCrawl", "Title")) 800 | item = self.ScannedItems.horizontalHeaderItem(2) 801 | item.setText(_translate("SubCrawl", "IMDb rating")) 802 | item = self.ScannedItems.horizontalHeaderItem(3) 803 | item.setText(_translate("SubCrawl", "Year")) 804 | item = self.ScannedItems.horizontalHeaderItem(4) 805 | item.setText(_translate("SubCrawl", "File location")) 806 | item = self.ScannedItems.horizontalHeaderItem(5) 807 | item.setText(_translate("SubCrawl", "Subtitles")) 808 | self.SelectAllRadio.setText(_translate("SubCrawl", "Select all")) 809 | self.RemoveEntryButton.setText(_translate("SubCrawl", "Remove entry")) 810 | self.ConfirmSelectionButton.setText(_translate("SubCrawl", "Confirm selection")) 811 | self.ClearDBButton.setText(_translate("SubCrawl", "Clear database")) 812 | self.CancelSelectionButton.setText(_translate("SubCrawl", "Cancel selection")) 813 | self.label_3.setText(_translate("SubCrawl", "Table view:")) 814 | self.ShowAllRadio.setText(_translate("SubCrawl", "Show all movies")) 815 | self.ShowSubsRadio.setText(_translate("SubCrawl", "Show movies with subtitles")) 816 | self.ShowNoSubsRadio.setText(_translate("SubCrawl", "Show movies without subtitles")) 817 | self.ScanLabel_2.setText(_translate("SubCrawl", "SubCrawl")) 818 | self.ScanLabel.setText(_translate("SubCrawl", "Scan folders\n" 819 | "for movies")) 820 | self.BrowseButton.setText(_translate("SubCrawl", "Browse")) 821 | self.SelectedFolderDisplay.setText(_translate("SubCrawl", "...")) 822 | self.CancelButton.setText(_translate("SubCrawl", "Cancel")) 823 | self.StartScanButton.setText(_translate("SubCrawl", "Start scan")) 824 | self.label.setText(_translate("SubCrawl", "Select subtitle language:")) 825 | self.LanguageComboBox.setItemText(0, _translate("SubCrawl", "Albanian")) 826 | self.LanguageComboBox.setItemText(1, _translate("SubCrawl", "Afghan")) 827 | self.LanguageComboBox.setItemText(2, _translate("SubCrawl", "New Item")) 828 | self.LanguageComboBox.setItemText(3, _translate("SubCrawl", "New Item")) 829 | self.LanguageComboBox.setItemText(4, _translate("SubCrawl", "New Item")) 830 | self.LanguageComboBox.setItemText(5, _translate("SubCrawl", "New Item")) 831 | self.LanguageComboBox.setItemText(6, _translate("SubCrawl", "New Item")) 832 | self.LanguageComboBox.setItemText(7, _translate("SubCrawl", "New Item")) 833 | self.LanguageComboBox.setItemText(8, _translate("SubCrawl", "New Item")) 834 | self.LanguageComboBox.setItemText(9, _translate("SubCrawl", "New Item")) 835 | self.LanguageComboBox.setItemText(10, _translate("SubCrawl", "New Item")) 836 | self.LanguageComboBox.setItemText(11, _translate("SubCrawl", "New Item")) 837 | self.LanguageComboBox.setItemText(12, _translate("SubCrawl", "New Item")) 838 | self.LanguageComboBox.setItemText(13, _translate("SubCrawl", "New Item")) 839 | self.LanguageComboBox.setItemText(14, _translate("SubCrawl", "New Item")) 840 | self.LanguageComboBox.setItemText(15, _translate("SubCrawl", "New Item")) 841 | self.LanguageComboBox.setItemText(16, _translate("SubCrawl", "New Item")) 842 | self.LanguageComboBox.setItemText(17, _translate("SubCrawl", "New Item")) 843 | self.LanguageComboBox.setItemText(18, _translate("SubCrawl", "New Item")) 844 | self.LanguageComboBox.setItemText(19, _translate("SubCrawl", "New Item")) 845 | self.LanguageComboBox.setItemText(20, _translate("SubCrawl", "New Item")) 846 | self.LanguageComboBox.setItemText(21, _translate("SubCrawl", "New Item")) 847 | self.LanguageComboBox.setItemText(22, _translate("SubCrawl", "New Item")) 848 | self.LanguageComboBox.setItemText(23, _translate("SubCrawl", "New Item")) 849 | self.LanguageComboBox.setItemText(24, _translate("SubCrawl", "New Item")) 850 | self.LanguageComboBox.setItemText(25, _translate("SubCrawl", "New Item")) 851 | self.LanguageComboBox.setItemText(26, _translate("SubCrawl", "New Item")) 852 | self.LanguageComboBox.setItemText(27, _translate("SubCrawl", "New Item")) 853 | self.LanguageComboBox.setItemText(28, _translate("SubCrawl", "New Item")) 854 | self.LanguageComboBox.setItemText(29, _translate("SubCrawl", "New Item")) 855 | self.LanguageComboBox.setItemText(30, _translate("SubCrawl", "New Item")) 856 | self.LanguageLabel.setText(_translate("SubCrawl", "Language: Albanian")) 857 | self.DownloadButton.setText(_translate("SubCrawl", "Download")) 858 | self.PromptLabel.setText(_translate("SubCrawl", "Ready to download!")) 859 | self.menuAbout.setTitle(_translate("SubCrawl", "About")) 860 | self.actionScanning.setText(_translate("SubCrawl", "Scanning")) 861 | self.actionSubtitles.setText(_translate("SubCrawl", "Subtitles")) 862 | self.actionAbout.setText(_translate("SubCrawl", "About")) 863 | self.actionContribute.setText(_translate("SubCrawl", "Contribute")) 864 | 865 | -------------------------------------------------------------------------------- /ui_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukaabra/SubCrawl/85da48f513dceed2ed9f9c60f09914996357e7f9/ui_example.png --------------------------------------------------------------------------------