├── README.md └── soap4me.bundle └── Contents ├── Code └── __init__.py ├── DefaultPrefs.json ├── Info.plist └── Resources ├── art.png ├── icon.png └── settings.png /README.md: -------------------------------------------------------------------------------- 1 | Это плагин для Plex, с помощью которого можно смотреть сериалы с сайта soap4.me 2 | ###Замечания 3 | Чтобы эпизод был отмечен, как просмотренный, воспроизведение должно дойти до конца. Эта функция не работает на Android (урезанный функционал клиента Plex). В настройках вы в любой момент можете выключить эту функцию, если вам нужно отмечать просмотр вручную. 4 | Субтитры не поддерживаются для каналов самим Plex, сделать пока ничего нельзя. 5 | 6 | ###Планы 7 | - Переработать вывод эпизодов (сортировка по качеству, отзвучке?) 8 | - Добавить дополнительные варианты сортировки списка сериалов (как на сайте) 9 | - Добавить поиск (?) 10 | - Интегрировать [soap4.me API](https://github.com/ufian/soap4me), написанную [@gimlis](https://twitter.com/gimlis) 11 | -------------------------------------------------------------------------------- /soap4me.bundle/Contents/Code/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # created by sergio 4 | # updated by kestl1st@gmail.com (@kestl) v.1.2.3 2016-08-01 5 | # updated by sergio v.1.2.2 2014-08-28 6 | 7 | import re,urllib2,base64,hashlib,md5,urllib 8 | import calendar 9 | from datetime import * 10 | import time 11 | 12 | VERSION = 2.0 13 | PREFIX = "/video/soap4me" 14 | TITLE = 'soap4.me' 15 | ART = 'art.png' 16 | ICON = 'icon.png' 17 | BASE_URL = 'http://soap4.me/' 18 | API_URL = 'http://soap4.me/api/' 19 | LOGIN_URL = 'http://soap4.me/login/' 20 | USER_AGENT = 'xbmc for soap' 21 | LOGGEDIN = False 22 | TOKEN = False 23 | SID = '' 24 | 25 | def Start(): 26 | ObjectContainer.art = R(ART) 27 | ObjectContainer.title1 = TITLE 28 | DirectoryObject.thumb = R(ICON) 29 | 30 | HTTP.CacheTime = CACHE_1HOUR 31 | HTTP.Headers['User-Agent'] = USER_AGENT 32 | HTTP.Headers['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' 33 | HTTP.Headers['Accept-Encoding'] ='gzip,deflate,sdch' 34 | HTTP.Headers['Accept-Language'] ='ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3' 35 | HTTP.Headers['x-api-token'] = TOKEN 36 | 37 | 38 | def Login(): 39 | global LOGGEDIN, SID, TOKEN 40 | 41 | if not Prefs['username'] and not Prefs['password']: 42 | return 2 43 | else: 44 | 45 | try: 46 | values = { 47 | 'login' : Prefs["username"], 48 | 'password' : Prefs["password"]} 49 | 50 | obj = JSON.ObjectFromURL(LOGIN_URL, values, encoding='utf-8', cacheTime=1,) 51 | except: 52 | obj=[] 53 | LOGGEDIN = False 54 | return 3 55 | SID = obj['sid'] 56 | TOKEN = obj['token'] 57 | if len(TOKEN) > 0: 58 | LOGGEDIN = True 59 | Dict['sid'] = SID 60 | Dict['token'] = TOKEN 61 | 62 | return 1 63 | else: 64 | LOGGEDIN = False 65 | Dict['sessionid'] = "" 66 | 67 | return 3 68 | 69 | 70 | def Thumb(url): 71 | if url=='': 72 | return Redirect(R(ICON)) 73 | else: 74 | try: 75 | data = HTTP.Request(url, cacheTime=CACHE_1WEEK).content 76 | return DataObject(data, 'image/jpeg') 77 | except: 78 | return Redirect(R(ICON)) 79 | 80 | 81 | @handler(PREFIX, TITLE, thumb=ICON, art=ART) 82 | def MainMenu(): 83 | 84 | oc = ObjectContainer() 85 | oc.add(DirectoryObject(key=Callback(Soaps, title2=u'Все сериалы', filter='all'), title=u'Все сериалы')) 86 | oc.add(DirectoryObject(key=Callback(Soaps, title2=u'Я смотрю', filter='watching'), title=u'Я смотрю')) 87 | oc.add(DirectoryObject(key=Callback(Soaps, title2=u'Новые эпизоды', filter='unwatched'), title=u'Новые эпизоды')) 88 | oc.add(PrefsObject(title=u'Настройки', thumb=R('settings.png'))) 89 | 90 | return oc 91 | 92 | 93 | @route(PREFIX+'/{filter}') 94 | def Soaps(title2, filter): 95 | 96 | logged = Login() 97 | if logged == 2: 98 | return MessageContainer( 99 | "Ошибка", 100 | "Ведите пароль и логин" 101 | ) 102 | 103 | elif logged == 3: 104 | return MessageContainer( 105 | "Ошибка", 106 | "Отказано в доступе" 107 | ) 108 | else: 109 | 110 | dir = ObjectContainer(title2=title2.decode()) 111 | if filter == 'all': 112 | url = API_URL + 'soap/' 113 | else: 114 | url = API_URL + 'soap/my/' 115 | obj = GET(url) 116 | 117 | obj=sorted(obj, key=lambda k: k['title']) 118 | 119 | for items in obj: 120 | if filter == 'unwatched' and items["unwatched"] == None: 121 | continue 122 | soap_title = items["title"] 123 | if filter != 'unwatched': 124 | title = soap_title 125 | else: 126 | title = items["title"]+ " (" +str(items["unwatched"])+ ")" 127 | summary = items["description"] 128 | poster = 'http://covers.s4me.ru/soap/big/'+items["sid"]+'.jpg' 129 | rating = float(items["imdb_rating"]) 130 | summary = summary.replace('"','"') 131 | fan = 'http://thetvdb.com/banners/fanart/original/'+items['tvdb_id']+'-1.jpg' 132 | id = items["sid"] 133 | thumb = Function(Thumb, url=poster) 134 | dir.add(TVShowObject(key=Callback(show_seasons, id = id, soap_title = soap_title, filter = filter, unwatched = filter=='unwatched'), rating_key = str(id), title = title, summary = summary, art = fan,rating = rating, thumb = thumb)) 135 | return dir 136 | 137 | 138 | @route(PREFIX+'/{filter}/{id}', unwatched=bool) 139 | def show_seasons(id, soap_title, filter, unwatched = False): 140 | 141 | dir = ObjectContainer(title2 = soap_title) 142 | url = API_URL + 'episodes/'+id 143 | data = GET(url) 144 | season = {} 145 | useason = {} 146 | s_length = {} 147 | 148 | #Log.Debug(str(data)) 149 | 150 | if unwatched: 151 | for episode in data: 152 | if episode['watched'] == None: 153 | if int(episode['season']) not in season: 154 | season[int(episode['season'])] = episode['season_id'] 155 | if int(episode['season']) not in useason.keys(): 156 | useason[int(episode['season'])] = [] 157 | useason[int(episode['season'])].append(int(episode['episode'])) 158 | elif int(episode['episode']) not in useason[int(episode['season'])]: 159 | useason[int(episode['season'])].append(int(episode['episode'])) 160 | else: 161 | for episode in data: 162 | if int(episode['season']) not in season: 163 | season[int(episode['season'])] = episode['season_id'] 164 | s_length[int(episode['season'])] = [episode['episode'],] 165 | else: 166 | if episode['episode'] not in s_length[int(episode['season'])]: 167 | s_length[int(episode['season'])].append(episode['episode']) 168 | 169 | for row in season: 170 | if unwatched: 171 | title = "%s сезон (%s)" % (row, len(useason[row])) 172 | else: 173 | title = "%s сезон" % (row) 174 | season_id = str(row) 175 | poster = "http://covers.s4me.ru/season/big/%s.jpg" % season[row] 176 | thumb=Function(Thumb, url=poster) 177 | dir.add(SeasonObject(key=Callback(show_episodes, sid = id, season = season_id, filter=filter, soap_title=soap_title, unwatched = unwatched), episode_count=len(s_length[row]) if s_length else len(useason[row]), show=soap_title, rating_key=str(row), title = title, thumb = thumb)) 178 | return dir 179 | 180 | @route(PREFIX+'/{filter}/{sid}/{season}', allow_sync=True, unwatched=bool) 181 | def show_episodes(sid, season, filter, soap_title, unwatched = False): 182 | 183 | dir = ObjectContainer(title2 = u'%s - %s сезон ' % (soap_title, season)) 184 | url = API_URL + 'episodes/'+sid 185 | data = GET(url) 186 | quality = Prefs["quality"] 187 | sort = Prefs["sorting"] 188 | show_only_hd = False 189 | 190 | if quality == "HD": 191 | for episode in data: 192 | if season == episode['season']: 193 | if episode['quality'] == '720p': 194 | show_only_hd = True 195 | break 196 | if sort != 'да': 197 | data = reversed(data) 198 | 199 | for row in data: 200 | if season == row['season']: 201 | 202 | if quality == "HD" and show_only_hd == True and row['quality'] != '720p': 203 | continue 204 | elif quality == "SD" and show_only_hd == False and row['quality'] != 'SD': 205 | continue 206 | else: 207 | if row['watched'] != None and unwatched: 208 | continue 209 | else: 210 | eid = row["eid"] 211 | ehash = row['hash'] 212 | sid = row['sid'] 213 | title = '' 214 | if not row['watched'] and not unwatched: 215 | title += '* ' 216 | title += "S" + str(row['season']) \ 217 | + "E" + str(row['episode']) + " | " \ 218 | + row['quality'].encode('utf-8') + " | " \ 219 | + row['translate'].encode('utf-8') + " | " \ 220 | + row['title_en'].encode('utf-8').replace(''', "'").replace("&", "&").replace('"','"') 221 | poster = "http://covers.s4me.ru/season/big/%s.jpg" % row['season_id'] 222 | summary = row['spoiler'] 223 | thumb = Function(Thumb, url=poster) 224 | parts = [PartObject(key=Callback(episode_url, sid=sid, eid=eid, ehash=ehash, part=0))] 225 | if Prefs["mark_watched"]=='да': 226 | parts.append(PartObject(key=Callback(episode_url, sid=sid, eid=eid, ehash=ehash, part=1))) 227 | dir.add(EpisodeObject( 228 | key=Callback(play_episode, sid = sid, eid = eid, ehash = ehash, row=row), 229 | rating_key='soap4me' + row["eid"], 230 | title=title, 231 | index=int(row['episode']), 232 | thumb=thumb, 233 | summary=summary, 234 | items=[MediaObject(parts=parts)] 235 | )) 236 | return dir 237 | 238 | def play_episode(sid, eid, ehash, row, *args, **kwargs): 239 | oc = ObjectContainer() 240 | parts = [PartObject(key=Callback(episode_url, sid=sid, eid=eid, ehash=ehash, part=0))] 241 | if Prefs["mark_watched"] == 'да': 242 | parts.append(PartObject(key=Callback(episode_url, sid=sid, eid=eid, ehash=ehash, part=1))) 243 | oc.add(EpisodeObject( 244 | key=Callback(play_episode, sid = sid, eid = eid, ehash = ehash, row=row), 245 | rating_key='soap4me' + row["eid"], 246 | items=[MediaObject( 247 | video_resolution = 720 if row['quality'].encode('utf-8')=='720p' else 400, 248 | video_codec = VideoCodec.H264, 249 | audio_codec = AudioCodec.AAC, 250 | container = Container.MP4, 251 | optimized_for_streaming = True, 252 | audio_channels = 2, 253 | parts = parts 254 | )] 255 | )) 256 | return oc 257 | 258 | def episode_url(sid, eid, ehash, part): 259 | token = Dict['token'] 260 | if part == 1: 261 | params = {"what": "mark_watched", "eid": eid, "token": token} 262 | data = JSON.ObjectFromURL("http://soap4.me/callback/", params, headers = {'x-api-token': Dict['token'], 'Cookie': 'PHPSESSID='+Dict['sid']}) 263 | return Redirect('https://soap4.me/assets/blank/blank1.mp4') 264 | 265 | myhash = hashlib.md5(str(token)+str(eid)+str(sid)+str(ehash)).hexdigest() 266 | params = {"what": "player", "do": "load", "token":token, "eid":eid, "hash":myhash} 267 | 268 | data = JSON.ObjectFromURL("http://soap4.me/callback/", params, headers = {'x-api-token': Dict['token'], 'Cookie': 'PHPSESSID='+Dict['sid']}) 269 | #Log.Debug('!!!!!!!!!!!!!!!!!! === ' + str(data)) 270 | if data["ok"] == 1: 271 | return Redirect("http://%s.soap4.me/%s/%s/%s/" % (data['server'], token, eid, myhash)) 272 | 273 | def GET(url): 274 | return JSON.ObjectFromURL(url, headers = {'x-api-token': Dict['token'], 'Cookie': 'PHPSESSID='+Dict['sid']}, cacheTime = 0) 275 | -------------------------------------------------------------------------------- /soap4me.bundle/Contents/DefaultPrefs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "username", 4 | "type": "text", 5 | "label": "Пользователь", 6 | "default": "" 7 | }, 8 | { 9 | "id": "password", 10 | "type": "text", 11 | "label": "Пароль", 12 | "default": "", 13 | "option": "hidden" 14 | }, 15 | { 16 | "id": "quality", 17 | "type": "text", 18 | "label": "Предпочитаемое качество", 19 | "default": "SD", 20 | "values": ["SD","HD","SD+HD"], 21 | "type": "enum" 22 | }, 23 | { 24 | "id": "sorting", 25 | "type": "text", 26 | "label": "Новые эпизоды сверху", 27 | "default": "да", 28 | "values": ["да","нет"], 29 | "type": "enum" 30 | }, 31 | { 32 | "id": "mark_watched", 33 | "type": "text", 34 | "label": "Автоматически отмечать просмотр эпизода", 35 | "default": "да", 36 | "values": ["да","нет"], 37 | "type": "enum" 38 | }, 39 | ] 40 | -------------------------------------------------------------------------------- /soap4me.bundle/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.plexapp.plugins.soap4me 7 | PlexFrameworkVersion 8 | 2 9 | PlexPluginClass 10 | Content 11 | PlexClientPlatforms 12 | * 13 | PlexMediaContainer 14 | 15 | MP4 16 | 17 | PlexAudioCodec 18 | 19 | ACC 20 | 21 | PlexVideoCodec 22 | 23 | H264 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /soap4me.bundle/Contents/Resources/art.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kestl/soap4plex/90e8a6f5a31e269700018b898373e921437a1a0c/soap4me.bundle/Contents/Resources/art.png -------------------------------------------------------------------------------- /soap4me.bundle/Contents/Resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kestl/soap4plex/90e8a6f5a31e269700018b898373e921437a1a0c/soap4me.bundle/Contents/Resources/icon.png -------------------------------------------------------------------------------- /soap4me.bundle/Contents/Resources/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kestl/soap4plex/90e8a6f5a31e269700018b898373e921437a1a0c/soap4me.bundle/Contents/Resources/settings.png --------------------------------------------------------------------------------