├── .gitignore ├── .travis.yml ├── AUTHORS ├── HISTORY ├── LICENSE ├── MANIFEST.in ├── README.md ├── cinemaflix ├── __init__.py ├── cli.py ├── config.ini ├── main.py ├── providers │ ├── __init__.py │ ├── constants.py │ ├── cpasbien.py │ ├── eztv.py │ ├── kickass.py │ ├── models.py │ ├── nyaa.py │ ├── provider.py │ ├── rarbg.py │ ├── rarbgapi.py │ ├── searchapi.py │ ├── tpb.py │ └── yts.py └── utils │ ├── __init__.py │ └── handler.py ├── examples └── demo.gif ├── setup.cfg ├── setup.py └── tests └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | #### joe made this: http://goel.io/joe 2 | 3 | #####=== Python ===##### 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | install: 5 | - python setup.py install 6 | - pip install pep8 7 | script: 8 | - 'pep8 cinemaflix/ --ignore E501,E731' -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | walid 2 | -------------------------------------------------------------------------------- /HISTORY: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | %%version%% (unreleased) 5 | ------------------------ 6 | 7 | - Remove strike as it is dead, improve rarbg. [walid] 8 | 9 | - Tpb: change domain. [walid] 10 | 11 | - Kickass: humanize torrent sizes. [walid] 12 | 13 | - Remove anime category, keep only movies. [walid] 14 | 15 | - Add install_requires to setup.py. [walid] 16 | 17 | - Release patch. [walid] 18 | 19 | - Merge branch 'master' of github.com:walidsa3d/cinemaflix. [walid] 20 | 21 | - Merge pull request #9 from gitter-badger/gitter-badge. [The Capsian] 22 | 23 | Add a Gitter chat badge to README.md 24 | 25 | - Add Gitter badge. [The Gitter Badger] 26 | 27 | 2.2.0 (2015-11-29) 28 | ------------------ 29 | 30 | - Enable yts (after switching to new url) [walid] 31 | 32 | 2.1.0 (2015-11-08) 33 | ------------------ 34 | 35 | - (cpasbien) change url. [walid] 36 | 37 | - Update readme. [walid] 38 | 39 | 2.0.0 (2015-11-06) 40 | ------------------ 41 | 42 | - Remove support for yts (no longer exists) [walid] 43 | 44 | 1.8.1 (2015-10-29) 45 | ------------------ 46 | 47 | - (eztv) use new working url. [walid] 48 | 49 | - (yts, eztv, strike) better error handling. [walid] 50 | 51 | - (main) fix unicode error. [walid] 52 | 53 | - Add banner. [walid] 54 | 55 | - Fix error in subtitle picker. [walid] 56 | 57 | - Improve exception handling for subtitles picker. [walid] 58 | 59 | - Fix MANIFEST.in. [walid] 60 | 61 | - Merge branch 'master' of github.com:walidsa3d/cinemaflix. [walid] 62 | 63 | - Add subtitle picker. [walid] 64 | 65 | closes #6 66 | 67 | 1.8.0 (2015-10-16) 68 | ------------------ 69 | 70 | - Add subtitle picker. [walid] 71 | 72 | closes #8 73 | 74 | - Rename cache to showscache. [walid] 75 | 76 | - Merge branch 'master' into dev. [walid] 77 | 78 | - (eztv) fix formatting, add comments. [walid] 79 | 80 | - Refactor code. [walid] 81 | 82 | - (eztv) refactoring. [walid] 83 | 84 | - Subtitles are now handled by sabertooth. [walid] 85 | 86 | - (eztv) get latest show episode. [walid] 87 | 88 | closes #8 89 | 90 | - (eztv) fix wrong numbering. [walid] 91 | 92 | 1.7.0 (2015-10-13) 93 | ------------------ 94 | 95 | - Make eztv work again. [walid] 96 | 97 | update shows cache 98 | allow search by season only 99 | 100 | - Some fixes. [walid] 101 | 102 | - Update README. [walid] 103 | 104 | - Better searchapi. [walid] 105 | 106 | use factory pattern 107 | 108 | - Fix issues when no subtitles are found. [walid] 109 | 110 | 1.6.0 (2015-10-12) 111 | ------------------ 112 | 113 | - Fix rarbg issues. [walid] 114 | 115 | closes #4 116 | 117 | 1.5.0 (2015-10-11) 118 | ------------------ 119 | 120 | - Use torrentutils package instead of local class. [walid] 121 | 122 | - Update README. [walid] 123 | 124 | - Fix setup.py. [walid] 125 | 126 | - Merge branch 'dev' [walid] 127 | 128 | - Fix formatting. [walid] 129 | 130 | - Update README. [walid] 131 | 132 | 1.4.0 (2015-10-05) 133 | ------------------ 134 | 135 | - Refactor code that parses pages. [walid] 136 | 137 | 1.3.0 (2015-10-05) 138 | ------------------ 139 | 140 | - Support webtorrent for streaming. [walid] 141 | 142 | 1.2.0 (2015-10-02) 143 | ------------------ 144 | 145 | - Bump version: 1.1.0 → 1.2.0. [walid] 146 | 147 | - Remove t411. [walid] 148 | 149 | removed temporarily till fixing issues 150 | 151 | 1.1.0 (2015-09-30) 152 | ------------------ 153 | 154 | - Check if peerflix is installed. [walid] 155 | 156 | closes #3 157 | 158 | 1.0.0 (2015-09-30) 159 | ------------------ 160 | 161 | - Update README. [walid] 162 | 163 | - Display top movies from rarbg, strike, cpasbien. [walid] 164 | 165 | fix cpasbien spelling mistake 166 | 167 | - Display top results from yts,tpb and kickass. [walid] 168 | 169 | - Bumped version 0.9. [walid] 170 | 171 | 0.9.0 (2015-09-06) 172 | ------------------ 173 | 174 | - New file: LICENSE new file: MANIFEST.in. [walid] 175 | 176 | - Refactorings. [walid] 177 | 178 | - Refactoring menu. [walid] 179 | 180 | - Update readme. [walid] 181 | 182 | - Fix tpb, remove limetorrents, change name. [walid] 183 | 184 | - Some fixes. [walid] 185 | 186 | - Remove oldpiratebay, fix strike and yts. [walid] 187 | 188 | - Some fixes. [walid] 189 | 190 | - Added config, better navigation, fixed tpb and limetorrents urls. 191 | [walid] 192 | 193 | - Better navigation, series searching. [walid] 194 | 195 | - Get first subtitle instead of best match. [walid] 196 | 197 | - Updated README. [walid] 198 | 199 | - Updated README. [walid] 200 | 201 | - Add series menu. [walid] 202 | 203 | 0.8.0 (2015-03-27) 204 | ------------------ 205 | 206 | - Changed project structure. [walid] 207 | 208 | 0.7.0 (2015-03-19) 209 | ------------------ 210 | 211 | - Support oldpiratebay.org. [walid] 212 | 213 | 0.6.0 (2015-03-18) 214 | ------------------ 215 | 216 | - Peerflix support. [walid] 217 | 218 | 0.5.0 (2015-03-18) 219 | ------------------ 220 | 221 | - Add support for cpasbien, t411. [walid] 222 | 223 | - Some fixes. [walid] 224 | 225 | - Better display, sort torrents. [walid] 226 | 227 | 0.4.0 (2015-03-15) 228 | ------------------ 229 | 230 | - Added support for limetorrents. [walid] 231 | 232 | 0.3.0 (2015-03-15) 233 | ------------------ 234 | 235 | - Added support for nyaa.se. [walid] 236 | 237 | - Fixed tpb torrent size. [walid] 238 | 239 | - Removed temp file. [walid] 240 | 241 | - Added torrentutil. [walid] 242 | 243 | - Add gitignore. [walid] 244 | 245 | 0.2.0 (2015-03-12) 246 | ------------------ 247 | 248 | - Support thepiratebay. [walid] 249 | 250 | - More providers, better display. [walid] 251 | 252 | 0.1.0 (2015-03-12) 253 | ------------------ 254 | 255 | - Added some providers. [walid] 256 | 257 | 258 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Walid Saad and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include cinemaflix/config.ini 2 | include cinemaflix/providers/showscache.json 3 | include README.md 4 | include LICENSE 5 | include AUTHORS 6 | include HISTORY 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cinemaflix 2 | 3 | [![Join the chat at https://gitter.im/walidsa3d/cinemaflix](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/walidsa3d/cinemaflix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | ![downloads](https://img.shields.io/pypi/dm/cinemaflix.svg) 5 | ![version](https://img.shields.io/pypi/v/cinemaflix.svg) 6 | ![license](https://img.shields.io/pypi/l/cinemaflix.svg) 7 | [![PEP8](https://img.shields.io/badge/code%20style-pep8-orange.svg)](https://www.python.org/dev/peps/pep-0008/) 8 | 9 | A console application for searching and streaming movies, anime and series. 10 | 11 | # Install (automatic) 12 | ``` 13 | $ pip install cinemaflix 14 | ``` 15 | # Install (manual) 16 | ``` 17 | $ git clone git@github.com:walidsa3d/cinemaflix.git 18 | $ cd cinemaflix 19 | $ python setup.py install 20 | ``` 21 | # Requirements 22 | This program requires peerflix. 23 | ``` 24 | $ npm install -g peerflix 25 | ``` 26 | You should also have one of the supported players installed : VLC, Mplayer or MPV. 27 | 28 | ## Supported Torrent Providers 29 | - Yts 30 | - Kickasstorrents 31 | - Strike 32 | - The Pirate Bay 33 | - Nyaa.se 34 | - Eztv 35 | - Cpasbien 36 | 37 | # Demo 38 | ![demo](https://cloud.githubusercontent.com/assets/821918/10253063/3cbba214-6933-11e5-9674-8aae44013463.gif) 39 | 40 | # License 41 | ``` 42 | The MIT License (MIT) 43 | 44 | Copyright (c) 2015 Walid Saad 45 | 46 | Permission is hereby granted, free of charge, to any person obtaining a copy of 47 | this software and associated documentation files (the "Software"), to deal in 48 | the Software without restriction, including without limitation the rights to 49 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 50 | the Software, and to permit persons to whom the Software is furnished to do so, 51 | subject to the following conditions: 52 | 53 | The above copyright notice and this permission notice shall be included in all 54 | copies or substantial portions of the Software. 55 | 56 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 57 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 58 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 59 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 60 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 61 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 62 | ``` -------------------------------------------------------------------------------- /cinemaflix/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "3.0.0" 2 | -------------------------------------------------------------------------------- /cinemaflix/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import os 5 | import sys 6 | 7 | from cinemaflix import __version__ 8 | from main import TSearch 9 | 10 | 11 | def cli(): 12 | parser = argparse.ArgumentParser(usage='-h for full usage') 13 | parser.add_argument( 14 | '-V', '--version', action='version', version=__version__) 15 | parser.parse_args() 16 | try: 17 | TSearch().main() 18 | except KeyboardInterrupt: 19 | print '\nExiting...' 20 | try: 21 | sys.exit(0) 22 | except SystemExit: 23 | os._exit(0) 24 | 25 | if __name__ == '__main__': 26 | cli() 27 | -------------------------------------------------------------------------------- /cinemaflix/config.ini: -------------------------------------------------------------------------------- 1 | # chosen player, possible values: mplayer,mpv,vlc 2 | player=vlc 3 | cache_path=~/Desktop/cflix/ 4 | #Language of subtitles 5 | subtitle_lang=en 6 | #minimum number of seeds 7 | min_seeds=0 8 | #max number of results to display 9 | max_results=20 10 | #Login Credentials for private providers 11 | -------------------------------------------------------------------------------- /cinemaflix/main.py: -------------------------------------------------------------------------------- 1 | import inquirer 2 | import os 3 | import providers.searchapi as searchapi 4 | import sys 5 | 6 | from configobj import ConfigObj 7 | from dateutil.parser import parse 8 | from prettytable import PrettyTable 9 | from sabertooth import subapi 10 | from termcolor import colored 11 | from utils.handler import TorrentHandler 12 | 13 | 14 | class TSearch(object): 15 | 16 | def display_results(self, torrent_list): 17 | for index, torrent in enumerate(torrent_list, start=1): 18 | index = colored(index, 'red', attrs=['blink']) 19 | title = colored(unicode(torrent.title), 'cyan') 20 | size = colored(unicode(torrent.size), 'green', attrs=['bold']) 21 | seeds = colored(torrent.seeds, 'white', attrs=['bold']) 22 | print u'{} {} {} {}'.format(index, title, size, seeds) 23 | 24 | def display_subtitles(self, data): 25 | output = PrettyTable(["I", "Lang", "Release", "Date"]) 26 | output.align = "l" 27 | for item in data: 28 | index = colored(item, 'red') 29 | lang = colored(data[item]['lang'], 'yellow', 'on_grey') 30 | dt = parse(data[item]['date']) 31 | date = colored(dt.strftime('%d/%m/%Y'), 'blue') 32 | release = colored( 33 | data[item]["movie"].encode('utf-8').strip(), 'green') 34 | output.add_row([index, lang, release, date]) 35 | print output 36 | 37 | def providers_menu(self): 38 | movie_sites = ['Yts', 'Kickass', 'ThePirateBay', 'Rarbg', 39 | 'Cpasbien'] 40 | sites = movie_sites 41 | subs = [ 42 | inquirer.List('site', 43 | message='Choose a Provider', 44 | choices=sites, 45 | ), 46 | ] 47 | site = inquirer.prompt(subs)['site'].lower() 48 | return site 49 | 50 | def main(self): 51 | # read config file 52 | configfile = os.path.join(os.path.dirname(__file__), 'config.ini') 53 | config = ConfigObj(configfile) 54 | player = config['player'] 55 | min_seeds = int(config['min_seeds']) 56 | max_results = int(config['max_results']) 57 | cache_path = os.path.expanduser(config['cache_path']) 58 | site = self.providers_menu() 59 | query = raw_input('Search: ') 60 | if query == '': 61 | search_results = searchapi.get_top(site) 62 | else: 63 | search_results = searchapi.search( 64 | query, site, sort='seeds', seeds=min_seeds, max=max_results) 65 | if search_results: 66 | self.display_results(search_results) 67 | else: 68 | print "No results Available" 69 | return 70 | user_input = raw_input('Pick Movie, [e]xit, [b]ack :\t') 71 | search_results = dict(enumerate(search_results, start=1)) 72 | while(not user_input.isdigit() or 73 | int(user_input) > len(search_results)): 74 | if user_input == 'e': 75 | sys.exit() 76 | elif user_input == 'b': 77 | os.system('clear') 78 | self.main() 79 | else: 80 | user_input = raw_input( 81 | 'Wrong Choice \nPick Movie, [e]xit, [b]ack :\t') 82 | movie = search_results[int(user_input)].title 83 | movie_url = search_results[int(user_input)].torrent_url 84 | handler = TorrentHandler(cache_path) 85 | subtitles = subapi.search( 86 | 'opensubtitles', movie, maxnumber=10, lang='en') 87 | if subtitles: 88 | subtitles = dict(enumerate(subtitles, start=1)) 89 | self.display_subtitles(subtitles) 90 | sub_choice = raw_input('Choose Subtitle:\t') 91 | if sub_choice not in map(str, range(1, 10)): 92 | print "Choice Not Available" 93 | return 94 | sub_choice = subtitles[int(sub_choice)] 95 | subtitle_file = subapi.download( 96 | 'opensubtitles', sub_choice, cache_path) 97 | print 'Streaming ' + movie 98 | handler.stream( 99 | 'peerflix', movie_url, player, subtitle=subtitle_file) 100 | else: 101 | print 'No subtitles found\n' 102 | streamit = raw_input('Stream movie? (y/n)\t') 103 | if streamit == "y": 104 | print 'Streaming ' + movie 105 | handler.stream('peerflix', movie_url, player, subtitle=None) 106 | else: 107 | return 108 | 109 | if __name__ == '__main__': 110 | TSearch().main() 111 | -------------------------------------------------------------------------------- /cinemaflix/providers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /cinemaflix/providers/constants.py: -------------------------------------------------------------------------------- 1 | TPB_URL = "https://thepiratebay.org" 2 | KICKASS_URL = "http://kat.cr" 3 | EZTV_URL = "http://eztvapi.co.za/" 4 | NYAA_URL = "http://www.nyaa.se/" 5 | YTS_URL = "http://yts.ag" 6 | RARBG_URL = "https://rarbg.to" 7 | CPABSIEN_URL = "http://www.cpasbien.io/recherche/films/" 8 | T411_URL = "https://api.t411.me/" 9 | STRIKE_URL = "https://getstrike.net" 10 | RARBG_API_URL = "https://torrentapi.org/pubapi_v2.php" 11 | -------------------------------------------------------------------------------- /cinemaflix/providers/cpasbien.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from bs4 import BeautifulSoup as BS 4 | from models import Torrent 5 | from provider import BaseProvider 6 | 7 | 8 | class Cpasbien(BaseProvider): 9 | 10 | def __init__(self, base_url): 11 | super(Cpasbien, self).__init__(base_url) 12 | 13 | def search(self, query): 14 | search_url = "{}{}{}".format( 15 | self.base_url, 16 | query, 17 | ".html,trie-seeds-d" 18 | ) 19 | response = requests.get(search_url).text 20 | torrents = self._parse_page(response) 21 | return torrents 22 | 23 | def _torrent_link(self, page_url): 24 | response = requests.get(page_url).text 25 | soup = BS(response, "lxml") 26 | relative_link = soup.find('a', id='telecharger').get('href') 27 | return "http://www.cpasbien.io" + relative_link 28 | 29 | def get_top(self): 30 | top_url = "http://www.cpasbien.io/view_cat.php?categorie=films&trie=seeds-d" 31 | response = requests.get(top_url).text 32 | torrents = self._parse_page(response) 33 | return torrents 34 | 35 | def _parse_page(self, page_text): 36 | soup = BS(page_text, "lxml") 37 | torrents = [] 38 | lines = soup.find_all(class_='ligne0') + soup.find_all(class_='ligne1') 39 | for line in lines: 40 | t = Torrent() 41 | t.title = line.find('a').text 42 | t.size = line.find(class_='poid').text 43 | t.seeds = int(line.find(class_='seed_ok').text) 44 | t.torrent_url = self._torrent_link(line.find('a').get('href')) 45 | torrents.append(t) 46 | return torrents 47 | -------------------------------------------------------------------------------- /cinemaflix/providers/eztv.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import re 4 | import requests 5 | 6 | from models import Torrent 7 | from operator import itemgetter 8 | from provider import BaseProvider 9 | 10 | 11 | class Eztv(BaseProvider): 12 | 13 | def __init__(self, base_url): 14 | super(Eztv, self).__init__(base_url) 15 | self.shows = [] 16 | self.shows_cache_path = os.path.join( 17 | os.path.dirname(__file__), 'showscache.json') 18 | 19 | def _get_all_shows(self): 20 | """get all shows supported by the api """ 21 | shows_url = self.base_url + 'shows/' 22 | data = requests.get(shows_url).json() 23 | shows = [] 24 | for url in [shows_url + unicode(x) for x in xrange(1, 16)]: 25 | data = requests.get(url).json() 26 | for show in data: 27 | shows.append({'id': show['imdb_id'], 'title': show['title']}) 28 | return shows 29 | 30 | def _search_show(self, query): 31 | with open(self.shows_cache_path, 'r') as f: 32 | shows = json.load(f) 33 | result = None 34 | for show in shows: 35 | match = query.lower() == show['title'].lower() 36 | if match: 37 | result = show 38 | break 39 | return result 40 | 41 | def _cache_shows(self): 42 | """get a list of all shows and save it locally""" 43 | shows = self._get_all_shows() 44 | with open(self.shows_cache_path, 'w') as f: 45 | f.write(json.dumps(shows)) 46 | 47 | def _get_show_episodes(self, show_id): 48 | """get all episodes of a show""" 49 | show_url = self.base_url + 'show/' + show_id 50 | data = requests.get(show_url).json() 51 | episodes = [] 52 | for episode in data['episodes']: 53 | episodes.append( 54 | { 55 | 'num': episode['episode'], 56 | 'season': episode['season'], 57 | 'title': episode['title'], 58 | 'torrent_url': episode['torrents']['0']['url'], 59 | 'seeds': episode['torrents']['0']['seeds'] 60 | }) 61 | episodes = sorted(episodes, key=lambda k: (k['season'], k['num'])) 62 | return episodes 63 | 64 | def _get_episode(self, show, season=None, episode=None): 65 | """get the given episode of a show's season""" 66 | torrents = [] 67 | all_episodes = self._get_show_episodes(show['id']) 68 | if episode is not None and season is not None: 69 | for ep in all_episodes: 70 | if season == ep['season'] and ep['num'] == episode: 71 | t = Torrent() 72 | t.title = '{}.S{}E{}:{}'.format( 73 | show['title'], ep['season'], ep['num'], ep['title']) 74 | t.torrent_url = ep['torrent_url'] 75 | t.seeds = ep['seeds'] 76 | torrents.append(t) 77 | break 78 | return torrents 79 | 80 | def _get_season_episodes(self, show, season): 81 | """get all episodes of a given season of a show""" 82 | torrents = [] 83 | all_episodes = self._get_show_episodes(show['id']) 84 | for ep in all_episodes: 85 | if season == ep['season']: 86 | t = Torrent() 87 | t.title = '{}.S{}E{}:{}'.format( 88 | show['title'], ep['season'], ep['num'], ep['title']) 89 | t.torrent_url = ep['torrent_url'] 90 | t.seeds = ep['seeds'] 91 | torrents.append(t) 92 | return torrents 93 | 94 | def _get_latest_episode(self, show): 95 | """get the latest episode of the latest season of a show""" 96 | torrents = [] 97 | all_episodes = self._get_show_episodes(show['id']) 98 | sorted_episodes = sorted( 99 | all_episodes, key=itemgetter('season', 'num'), reverse=True) 100 | last_ep = {k: str(v) for k, v in sorted_episodes[0].iteritems()} 101 | t = Torrent() 102 | t.title = '{}.S{}E{}:{}'.format( 103 | show['title'], last_ep['season'], last_ep['num'], last_ep['title']) 104 | t.torrent_url = last_ep['torrent_url'] 105 | t.seeds = int(last_ep['seeds']) 106 | torrents.append(t) 107 | return torrents 108 | 109 | def _query(self, showname, season=None, episode=None, latest=False): 110 | try: 111 | show = self._search_show(showname) 112 | except Exception: 113 | return 114 | if show is None: 115 | print 'Show Not Found' 116 | return 117 | if latest: 118 | torrents = self._get_latest_episode(show) 119 | elif episode is None: 120 | season = int(season) 121 | torrents = self._get_season_episodes(show, season) 122 | else: 123 | season = int(season) 124 | episode = int(episode) 125 | torrents = self._get_episode( 126 | show, season, episode) 127 | return torrents 128 | 129 | def search(self, query): 130 | """parse query and get search results """ 131 | results = [] 132 | ep_match = re.match(r"(([a-zA-Z]+\s*)+)(\s[0-9]+\s[0-9]+)$", query) 133 | season_match = re.match(r"(([a-zA-Z]+\s*)+)(\s[0-9]+)$", query) 134 | latest_match = re.match(r"(([a-zA-Z]+\s*)+)(latest)$", query) 135 | if ep_match: 136 | show = ep_match.group(1) 137 | season = ep_match.group(3).strip().split(' ')[0] 138 | episode = ep_match.group(3).strip().split(' ')[1] 139 | results = self._query(show, season, episode) 140 | elif season_match: 141 | show = season_match.group(1) 142 | season = season_match.group(3).strip().split(' ')[0] 143 | results = self._query(show, season) 144 | elif latest_match: 145 | show = latest_match.group(1).strip() 146 | results = self._query(show, latest=True) 147 | else: 148 | raise ValueError('Badly Formatted Query') 149 | return results 150 | 151 | def get_top(self): 152 | return [] 153 | -------------------------------------------------------------------------------- /cinemaflix/providers/kickass.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | from humanize import naturalsize 5 | from bs4 import BeautifulSoup as BS 6 | from models import Torrent 7 | from provider import BaseProvider 8 | 9 | 10 | class Kickass(BaseProvider): 11 | 12 | def __init__(self, base_url): 13 | super(Kickass, self).__init__(base_url) 14 | 15 | def search(self, query): 16 | payload = {'q': query, 'field': 'seeder', 'order': 'desc', 'page': '1'} 17 | search_url = self.base_url + '/json.php' 18 | data = requests.get( 19 | search_url, params=payload, headers=self.headers).json() 20 | torrents = [] 21 | for movie in data['list']: 22 | t = Torrent() 23 | t.title = movie['title'] 24 | t.seeds = int(movie['seeds']) 25 | t.size = naturalsize(movie['size']) 26 | t.torrent_url = movie['torrentLink'] 27 | torrents.append(t) 28 | return torrents 29 | 30 | def get_top(self): 31 | search_url = self.base_url + '/movies' 32 | data = requests.get(search_url, headers=self.headers).text 33 | soup = BS(data, "lxml") 34 | torrents = [] 35 | table = soup.find(class_="data") 36 | for row in table.find_all('tr')[1:]: 37 | cells = row.find_all('td') 38 | t = Torrent() 39 | t.title = cells[0].find(class_="cellMainLink").text 40 | t.torrent_url = cells[0].find_all("a")[3].get('href') 41 | t.size = cells[1].text 42 | t.seeds = int(cells[4].text) 43 | torrents.append(t) 44 | return torrents 45 | -------------------------------------------------------------------------------- /cinemaflix/providers/models.py: -------------------------------------------------------------------------------- 1 | class Torrent(object): 2 | 3 | def __init__(self): 4 | self.title = '' 5 | self.torrent_url = '' 6 | self.seeds = 0 7 | self.size = '' 8 | 9 | def __eq__(self, other): 10 | return self.title == other.title 11 | 12 | def __repr__(self): 13 | return '(%s, %s,%s,%s)' % ( 14 | self.torrent_url, 15 | self.title, 16 | repr(self.seeds), 17 | self.size 18 | ) 19 | 20 | def __str__(self): 21 | return self.title 22 | -------------------------------------------------------------------------------- /cinemaflix/providers/nyaa.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from bs4 import BeautifulSoup as bs 4 | from models import Torrent 5 | from provider import BaseProvider 6 | 7 | 8 | class Nyaa(BaseProvider): 9 | 10 | def __init__(self, base_url): 11 | super(Nyaa, self).__init__(base_url) 12 | 13 | def search(self, query): 14 | search_url = self.base_url 15 | payload = {'page': 'search', 'term': query, 16 | 'sort': '2', 'cats': '1_0', 'filter': '0'} 17 | torrents = [] 18 | response = requests.get( 19 | search_url, params=payload, headers=self.headers).text 20 | soup = bs(response, "lxml") 21 | table = soup.find('table', class_='tlist') 22 | for tr in table.find_all('tr')[1:]: 23 | t = Torrent() 24 | cols = tr.findAll('td') 25 | t.title = cols[1].find('a').text 26 | t.size = cols[3].text 27 | t.seeds = cols[4].text 28 | t.torrent_url = cols[2].find('a').get('href') + "&magnet=1" 29 | torrents.append(t) 30 | return torrents 31 | -------------------------------------------------------------------------------- /cinemaflix/providers/provider.py: -------------------------------------------------------------------------------- 1 | 2 | class BaseProvider(object): 3 | 4 | """A base class for search providers""" 5 | 6 | def __init__(self, base_url): 7 | self.base_url = base_url 8 | self.headers = {'Referer': self.base_url, 9 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36'} 10 | 11 | def search(self, query): 12 | pass 13 | 14 | def get_top(self): 15 | pass 16 | -------------------------------------------------------------------------------- /cinemaflix/providers/rarbg.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | import base64 5 | import bencode 6 | import hashlib 7 | 8 | import tempfile 9 | from humanize import naturalsize 10 | from bs4 import BeautifulSoup as BS 11 | from models import Torrent 12 | from provider import BaseProvider 13 | 14 | 15 | class Rarbg(BaseProvider): 16 | 17 | def __init__(self, base_url): 18 | super(Rarbg, self).__init__(base_url) 19 | 20 | def search(self, query): 21 | payload = {'category': '14;48;17;44;45;47;42;46', 22 | 'search': query, 'order': 'seeder', 'by': 'DESC'} 23 | search_url = self.base_url + '/torrents.php' 24 | cookies = {'7fAY799j': 'VtdTzG69'} 25 | response = requests.get( 26 | search_url, headers=self.headers, params=payload, cookies=cookies) 27 | torrents = self._parse_page(response.text) 28 | return torrents 29 | 30 | def get_top(self): 31 | top_url = "https://rarbg.to/torrents.php?category=14;48;17;44;45;47;42;46&search=rarbg&order=seeders&by=DESC&page=1" 32 | cookies = {'7fAY799j': 'VtdTzG69'} 33 | response = requests.get( 34 | top_url, headers=self.headers, cookies=cookies).text 35 | torrents = self._parse_page(response) 36 | return torrents 37 | 38 | def _parse_page(self, page_text): 39 | soup = BS(page_text, "lxml") 40 | tabl = soup.find('table', class_='lista2t') 41 | torrents = [] 42 | for tr in tabl.find_all('tr')[1:]: 43 | rows = tr.find_all('td') 44 | try: 45 | t = Torrent() 46 | t.title = rows[1].find('a').text 47 | rarbg_id = rows[1].find('a')['href'].strip('/torrent/') 48 | title = requests.utils.quote(t.title) + "-[rarbg.com].torrent" 49 | download_url = self.base_url + "/download.php?id=%s&f=%s" % ( 50 | rarbg_id, title) 51 | t.torrent_url = self._to_magnet(download_url) 52 | t.size = naturalsize(rows[3].text) 53 | t.seeds = int(rows[4].text) 54 | torrents.append(t) 55 | except bencode.BTL.BTFailure: 56 | pass 57 | return torrents 58 | 59 | def _to_magnet(self, torrent_link): 60 | """converts a torrent file to a magnet link""" 61 | self.headers.update({'Referer': 'https://rarbg.to/torrent/'}) 62 | headers = self.headers 63 | cookies = {'7fAY799j': 'VtdTzG69'} 64 | response = requests.get( 65 | torrent_link, headers=headers, timeout=20, cookies=cookies) 66 | temp = tempfile.TemporaryFile() 67 | temp.write(response.content) 68 | temp.seek(0) 69 | torrent = temp.read() 70 | temp.close() 71 | metadata = bencode.bdecode(torrent) 72 | hashcontents = bencode.bencode(metadata['info']) 73 | digest = hashlib.sha1(hashcontents).digest() 74 | b32hash = base64.b32encode(digest) 75 | magneturi = 'magnet:?xt=urn:btih:%s' % b32hash 76 | return magneturi 77 | -------------------------------------------------------------------------------- /cinemaflix/providers/rarbgapi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import requests 3 | 4 | from models import Torrent 5 | from provider import BaseProvider 6 | from humanize import naturalsize 7 | 8 | 9 | class RarbgAPI(BaseProvider): 10 | 11 | def __init__(self, base_url): 12 | super(RarbgAPI, self).__init__(base_url) 13 | 14 | def _get_token(self): 15 | token_payload = { 16 | 'get_token': 'get_token', 17 | 'app_id': 'cinemaflix', 18 | } 19 | response = requests.get(self.base_url, params=token_payload).json() 20 | self.token = response['token'] 21 | 22 | def search(self, query): 23 | self._get_token() 24 | search_payload = { 25 | 'sort': 'seeders', 26 | 'category': 'movies', 27 | 'mode': 'search', 28 | 'app_id': 'xxx', 29 | 'format': 'json_extended', 30 | 'search_string': query, 31 | 'token': self.token, 32 | 33 | } 34 | results = requests.get(self.base_url, params=search_payload).json() 35 | torrents = [] 36 | for result in results['torrent_results']: 37 | t = Torrent() 38 | t.title = result['title'] 39 | t.seeds = result['seeders'] 40 | t.size = naturalsize(result['size']) 41 | t.torrent_url = result['download'] 42 | torrents.append(t) 43 | return torrents 44 | -------------------------------------------------------------------------------- /cinemaflix/providers/searchapi.py: -------------------------------------------------------------------------------- 1 | 2 | from constants import * 3 | from cpasbien import Cpasbien 4 | from kickass import Kickass 5 | from operator import attrgetter 6 | from rarbg import Rarbg 7 | from tpb import TPB 8 | from yts import YTS 9 | from rarbgapi import RarbgAPI 10 | 11 | providers = { 12 | "kickass": (Kickass, KICKASS_URL), 13 | "rarbg": (RarbgAPI, RARBG_API_URL), 14 | "yts": (YTS, YTS_URL), 15 | "thepiratebay": (TPB, TPB_URL), 16 | "cpasbien": (Cpasbien, CPABSIEN_URL), 17 | } 18 | 19 | 20 | def search(query, provider, sort=None, seeds=0, max=0): 21 | sorts = ['seeds', 'size'] 22 | provider_class, site_url = providers.get(provider, ('tpb', TPB_URL)) 23 | results = provider_class(site_url).search(query) 24 | if results: 25 | sorted_results = _sort_results(results, sort) if sort in sorts else results 26 | filtered_results = filter(lambda x: x.seeds >= seeds, sorted_results) 27 | results = filtered_results[:max] 28 | return results 29 | 30 | 31 | def _sort_results(torrent_list, criteria): 32 | return sorted(torrent_list, key=attrgetter(criteria), reverse=True) 33 | 34 | 35 | def get_top(provider): 36 | provider_class, site_url = providers.get(provider, ('tpb', TPB_URL)) 37 | results = provider_class(site_url).get_top() 38 | return results 39 | -------------------------------------------------------------------------------- /cinemaflix/providers/tpb.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import re 4 | 5 | import requests 6 | 7 | from bs4 import BeautifulSoup as BS 8 | from models import Torrent 9 | from provider import BaseProvider 10 | 11 | 12 | class TPB(BaseProvider): 13 | 14 | def __init__(self, base_url): 15 | super(TPB, self).__init__(base_url) 16 | 17 | def search(self, query): 18 | search_url = self.base_url + "/search/" + query + "/0/7/0" 19 | response = requests.get(search_url).text 20 | torrents = self._parse_page(response) 21 | return torrents 22 | 23 | def get_top(self): 24 | search_url = self.base_url + "/browse/201/0/7/0" 25 | response = requests.get(search_url).text 26 | torrents = self._parse_page(response) 27 | return torrents 28 | 29 | def _parse_page(self, page_text): 30 | soup = BS(page_text, "lxml") 31 | torrents = [] 32 | table = soup.find(id="searchResult") 33 | for row in table.find_all('tr')[1:30]: 34 | t = Torrent() 35 | cells = row.find_all('td') 36 | a = cells[1].find_all('a') 37 | t.title = a[0].text 38 | t.torrent_url = a[1]['href'] 39 | t.seeds = int(cells[2].text) 40 | pattern = re.compile("Uploaded (.*), Size (.*), ULed by (.*)") 41 | match = pattern.match(cells[1].font.text) 42 | t.size = match.groups()[1].replace('xa0', ' ') 43 | torrents.append(t) 44 | return torrents 45 | -------------------------------------------------------------------------------- /cinemaflix/providers/yts.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from models import Torrent 4 | from provider import BaseProvider 5 | from humanize import naturalsize 6 | 7 | class YTS(BaseProvider): 8 | 9 | def __init__(self, base_url): 10 | super(YTS, self).__init__(base_url) 11 | 12 | def search(self, query): 13 | payload = { 14 | 'query_term': query, 'sort': 'title', 'order': 'desc', 'set': '1'} 15 | search_url = self.base_url + '/api/v2/list_movies.json' 16 | try: 17 | response = requests.get( 18 | search_url, params=payload, headers=self.headers).json() 19 | except Exception: 20 | return 21 | torrents = [] 22 | for movie in response['data']['movies']: 23 | for torrent in movie['torrents']: 24 | t = Torrent() 25 | t.title = movie['title_long'] + " " + torrent['quality'] 26 | t.seeds = torrent['seeds'] 27 | t.size = torrent['size'] 28 | t.torrent_url = torrent['url'] 29 | torrents.append(t) 30 | return torrents 31 | 32 | def get_top(self): 33 | payload = { 34 | 'sort': 'date_added', 35 | 'order': 'desc', 36 | 'set': '1', 37 | 'limit': 20 38 | } 39 | search_url = self.base_url + '/api/v2/list_movies.json' 40 | try: 41 | response = requests.get( 42 | search_url, params=payload, headers=self.headers).json() 43 | except Exception: 44 | return 45 | torrents = [] 46 | for movie in response['data']['movies']: 47 | for torrent in movie['torrents']: 48 | t = Torrent() 49 | t.title = movie['title_long'] + " " + torrent['quality'] 50 | t.seeds = torrent['seeds'] 51 | t.size = torrent['size'] 52 | t.torrent_url = torrent['url'] 53 | torrents.append(t) 54 | return torrents 55 | -------------------------------------------------------------------------------- /cinemaflix/utils/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.9.0" 2 | -------------------------------------------------------------------------------- /cinemaflix/utils/handler.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | 5 | class ResourceNotFoundException(Exception): 6 | pass 7 | 8 | 9 | class TorrentHandler(object): 10 | 11 | def __init__(self, cache_path): 12 | self.cache_path = cache_path 13 | self.players = ['vlc', 'mpv', 'mplayer'] 14 | 15 | def stream_with_peerflix(self, link, player, subtitle=None): 16 | if player not in self.players: 17 | raise ResourceNotFoundException('Player Not Found') 18 | if not self.which('peerflix'): 19 | raise ResourceNotFoundException('Peerflix Not Found') 20 | command = 'peerflix "{}" --{} -f {} -d'.format( 21 | link, player, self.cache_path) 22 | if subtitle is not None: 23 | command += ' --subtitles "%s"' % subtitle 24 | subprocess.Popen(command, shell=True) 25 | 26 | def stream_with_webtorrent(self, link, player, subtitle=None): 27 | if player not in self.players: 28 | raise ResourceNotFoundException('Player Not Found') 29 | if not self.which('webtorrent'): 30 | raise ResourceNotFoundException('WebTorrent Not Found') 31 | command = 'webtorrent "{}" --{} -o {}'.format( 32 | link, player, self.cache_path) 33 | if subtitle is not None: 34 | command = command + ' --subtitles "%s"' % subtitle 35 | subprocess.Popen(command) 36 | 37 | def stream(self, handler, link, player, subtitle=None): 38 | if handler == 'peerflix': 39 | self.stream_with_peerflix(link, player, subtitle) 40 | elif handler == 'webtorrent': 41 | self.stream_with_webtorrent(link, player, subtitle) 42 | else: 43 | raise ResourceNotFoundException('handler not supported') 44 | 45 | @staticmethod 46 | def which(cmd): 47 | inst = lambda x: any(os.access(os.path.join(path, x), os.X_OK) for path 48 | in os.environ["PATH"].split(os.pathsep)) 49 | return inst(cmd) 50 | -------------------------------------------------------------------------------- /examples/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walidsa3d/cinemaflix/b367a511628da8d1617c2909a236beaf2a338891/examples/demo.gif -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 3.0.0 3 | commit = False 4 | tag = False 5 | 6 | [bumpversion:file:setup.py] 7 | 8 | [bumpversion:file:cinemaflix/__init__.py] 9 | 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages 2 | from setuptools import setup 3 | 4 | try: 5 | from pypandoc import convert 6 | read_md = lambda f: convert(f, 'rst') 7 | except ImportError: 8 | print( 9 | "warning: pypandoc module not found, could not convert Markdown to RST") 10 | read_md = lambda f: open(f, 'r').read() 11 | 12 | setup( 13 | name='cinemaflix', 14 | version='3.0.0', 15 | description="A command line tool to find and play movies online", 16 | long_description=read_md('README.md'), 17 | author='Walid Saad', 18 | author_email='walid.sa3d@gmail.com', 19 | url='https://github.com/walidsa3d/cinemaflix', 20 | license="MIT", 21 | keywords="cli torrent movies", 22 | packages=find_packages(), 23 | include_package_data=True, 24 | install_requires=['torrentutils', 25 | 'requests', 26 | 'beautifulsoup4', 27 | 'sabertooth', 28 | 'inquirer', 29 | 'termcolor', 30 | 'prettytable'], 31 | entry_points={"console_scripts": ["cinemaflix=cinemaflix.cli:cli"]}, 32 | classifiers=[ 33 | 'Development Status :: 4 - Beta', 34 | 'Environment :: Console', 35 | 'Intended Audience :: End Users/Desktop', 36 | 'License :: OSI Approved :: MIT License', 37 | 'Operating System :: OS Independent', 38 | 'Natural Language :: English', 39 | 'Operating System :: OS Independent', 40 | 'Programming Language :: Python :: 2', 41 | 'Topic :: Multimedia :: Video', 42 | 'Topic :: Utilities' 43 | ] 44 | ) 45 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | --------------------------------------------------------------------------------