| |
| |
| """ |
| PeacocksHouse v3.0 |
| Free Streaming App - Actually Works Edition |
| No lag, working sources, proper episode picker. |
| """ |
|
|
| import sys, os, json, urllib.request, urllib.parse, hashlib |
| from pathlib import Path |
| from datetime import datetime |
|
|
| from PyQt6.QtWidgets import * |
| from PyQt6.QtCore import * |
| from PyQt6.QtGui import * |
| from PyQt6.QtWebEngineWidgets import QWebEngineView |
| from PyQt6.QtWebEngineCore import QWebEnginePage, QWebEngineSettings |
|
|
| APP = "PeacocksHouse" |
| VER = "3.0" |
| DATA = Path.home() / ".peacockshouse" |
| DATA.mkdir(parents=True, exist_ok=True) |
| (DATA / "cache").mkdir(exist_ok=True) |
| CFG = DATA / "config.json" |
| LIB = DATA / "library.json" |
|
|
| def cfg_load(): |
| try: return json.loads(CFG.read_text()) if CFG.exists() else {} |
| except: return {} |
| def cfg_save(d): CFG.write_text(json.dumps(d)) |
| def get_key(): return cfg_load().get("key","") |
| def set_key(k): d=cfg_load(); d["key"]=k; cfg_save(d) |
| def lib_load(): |
| try: return json.loads(LIB.read_text()) if LIB.exists() else {"wl":[],"hist":[]} |
| except: return {"wl":[],"hist":[]} |
| def lib_save(d): LIB.write_text(json.dumps(d)) |
|
|
| |
| class TMDB: |
| B="https://api.themoviedb.org/3" |
| I="https://image.tmdb.org/t/p" |
| @classmethod |
| def get(cls,ep,params=None): |
| k=get_key() |
| if not k: return None |
| p=params or {}; p['api_key']=k |
| try: |
| r=urllib.request.urlopen(urllib.request.Request(f"{cls.B}{ep}?{urllib.parse.urlencode(p)}",headers={'User-Agent':'PeacocksHouse'}),timeout=8) |
| return json.loads(r.read()) |
| except: return None |
| @classmethod |
| def trending(cls): return cls.get("/trending/all/week") |
| @classmethod |
| def movies(cls): return cls.get("/movie/popular") |
| @classmethod |
| def tv(cls): return cls.get("/tv/popular") |
| @classmethod |
| def search(cls,q): return cls.get("/search/multi",{"query":q}) |
| @classmethod |
| def tv_details(cls,tid): return cls.get(f"/tv/{tid}") |
| @classmethod |
| def season(cls,tid,sn): return cls.get(f"/tv/{tid}/season/{sn}") |
| @classmethod |
| def img(cls,p,sz="w342"): return f"{cls.I}/{sz}{p}" if p else "" |
|
|
| |
| SOURCES = [ |
| ("MoviesAPI", "https://moviesapi.club/movie/{id}", "https://moviesapi.club/tv/{id}-{s}-{e}"), |
| ("VidLink", "https://vidlink.pro/movie/{id}", "https://vidlink.pro/tv/{id}/{s}/{e}"), |
| ("VidSrc.cc", "https://vidsrc.cc/v2/embed/movie/{id}", "https://vidsrc.cc/v2/embed/tv/{id}/{s}/{e}"), |
| ("SuperEmbed", "https://multiembed.mov/?video_id={id}&tmdb=1", "https://multiembed.mov/?video_id={id}&tmdb=1&s={s}&e={e}"), |
| ("2Embed", "https://www.2embed.skin/embed/movie/{id}", "https://www.2embed.skin/embedtv/{id}&s={s}&e={e}"), |
| ("NontonGo", "https://www.nontongo.win/embed/movie/{id}", "https://www.nontongo.win/embed/tv/{id}/{s}/{e}"), |
| ] |
|
|
| import webbrowser |
|
|
| |
| class W(QThread): |
| done=pyqtSignal(object) |
| def __init__(self,fn,*a):super().__init__();self.fn=fn;self.a=a |
| def run(self):self.done.emit(self.fn(*self.a)) |
|
|
| class ImgW(QThread): |
| loaded=pyqtSignal(str,bytes) |
| def __init__(self,url,key):super().__init__();self.url=url;self.key=key |
| def run(self): |
| cp=DATA/"cache"/f"{self.key}.jpg" |
| try: |
| if cp.exists(): self.loaded.emit(self.key,cp.read_bytes()) |
| else: |
| d=urllib.request.urlopen(urllib.request.Request(self.url,headers={'User-Agent':'Mozilla/5.0'}),timeout=8).read() |
| cp.write_bytes(d); self.loaded.emit(self.key,d) |
| except:pass |
|
|
| |
| S=""" |
| QMainWindow,QWidget{background:#0a0a14;color:#ddd} |
| QLabel{background:transparent} |
| QLineEdit{background:#101020;border:1px solid #222;border-radius:6px;padding:8px 14px;color:#eee;font-size:13px} |
| QLineEdit:focus{border-color:#00bfa5} |
| QPushButton{background:#151528;border:1px solid #252540;border-radius:5px;padding:7px 14px;color:#ccc;font-weight:bold;font-size:11px} |
| QPushButton:hover{background:#1c1c3a;border-color:#00bfa5;color:#fff} |
| QPushButton#play{background:#00bfa5;border:none;color:#000;padding:10px 24px;font-size:13px;border-radius:6px} |
| QPushButton#play:hover{background:#00e5c3} |
| QPushButton#src{background:#252540;border:none;color:#aaa;padding:6px 12px;border-radius:4px;font-size:10px} |
| QPushButton#src:hover{background:#00bfa5;color:#000} |
| QComboBox{background:#101020;border:1px solid #222;border-radius:5px;padding:6px 10px;color:#ddd} |
| QScrollArea{border:none} |
| QScrollBar:vertical{width:6px;background:#0a0a14} |
| QScrollBar::handle:vertical{background:#333;border-radius:3px} |
| QScrollBar::handle:vertical:hover{background:#00bfa5} |
| QScrollBar::add-line:vertical,QScrollBar::sub-line:vertical{height:0} |
| """ |
|
|
| |
| class Card(QFrame): |
| clicked=pyqtSignal(dict) |
| def __init__(self,item): |
| super().__init__() |
| self._item=item |
| self.setFixedSize(140,240) |
| self.setCursor(Qt.CursorShape.PointingHandCursor) |
| self.setStyleSheet("QFrame{background:#101020;border:1px solid #1a1a30;border-radius:6px}QFrame:hover{border-color:#00bfa5}") |
| l=QVBoxLayout(self);l.setContentsMargins(0,0,0,6);l.setSpacing(4) |
| self._img=QLabel();self._img.setFixedSize(138,190);self._img.setAlignment(Qt.AlignmentFlag.AlignCenter) |
| self._img.setStyleSheet("background:#0a0a14;border-radius:6px 6px 0 0;color:#444;font-size:10px") |
| self._img.setText("...") |
| l.addWidget(self._img) |
| t=item.get('title') or item.get('name') or "?" |
| tl=QLabel(t[:18]+".." if len(t)>18 else t) |
| tl.setStyleSheet("color:#ccc;font-size:10px;font-weight:bold;padding:0 4px") |
| l.addWidget(tl) |
| r=item.get('vote_average',0) |
| if r: |
| rl=QLabel(f"β
{r:.1f}") |
| rl.setStyleSheet("color:#ffd700;font-size:9px;padding:0 4px") |
| l.addWidget(rl) |
| |
| pp=item.get('poster_path') |
| if pp: |
| url=TMDB.img(pp,"w185") |
| key=hashlib.md5(url.encode()).hexdigest() |
| self._ldr=ImgW(url,key) |
| self._ldr.loaded.connect(self._set_img) |
| self._ldr.start() |
| def _set_img(self,k,data): |
| px=QPixmap();px.loadFromData(data) |
| if not px.isNull(): |
| self._img.setPixmap(px.scaled(138,190,Qt.AspectRatioMode.KeepAspectRatioByExpanding,Qt.TransformationMode.SmoothTransformation).copy(0,0,138,190)) |
| def mousePressEvent(self,e):self.clicked.emit(self._item) |
|
|
| |
| class Row(QWidget): |
| card_clicked=pyqtSignal(dict) |
| def __init__(self,title): |
| super().__init__() |
| l=QVBoxLayout(self);l.setContentsMargins(0,0,0,12);l.setSpacing(6) |
| lbl=QLabel(title);lbl.setFont(QFont("Segoe UI",14,QFont.Weight.Bold));lbl.setStyleSheet("color:#eee") |
| l.addWidget(lbl) |
| sc=QScrollArea();sc.setFixedHeight(255);sc.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) |
| sc.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff);sc.setWidgetResizable(True) |
| self._con=QWidget();self._hl=QHBoxLayout(self._con);self._hl.setContentsMargins(0,0,0,0);self._hl.setSpacing(10);self._hl.addStretch() |
| sc.setWidget(self._con);l.addWidget(sc) |
| def set_items(self,items): |
| while self._hl.count()>1: |
| w=self._hl.takeAt(0).widget() |
| if w:w.deleteLater() |
| for d in (items or[])[:20]: |
| c=Card(d);c.clicked.connect(self.card_clicked.emit) |
| self._hl.insertWidget(self._hl.count()-1,c) |
|
|
| |
| class Home(QWidget): |
| open_item=pyqtSignal(dict) |
| def __init__(self): |
| super().__init__() |
| sc=QScrollArea();sc.setWidgetResizable(True);sc.setStyleSheet("border:none") |
| con=QWidget();cl=QVBoxLayout(con);cl.setContentsMargins(16,16,16,16);cl.setSpacing(0) |
| self._r1=Row("π₯ Trending");self._r1.card_clicked.connect(self.open_item) |
| self._r2=Row("π¬ Movies");self._r2.card_clicked.connect(self.open_item) |
| self._r3=Row("πΊ TV Shows");self._r3.card_clicked.connect(self.open_item) |
| cl.addWidget(self._r1);cl.addWidget(self._r2);cl.addWidget(self._r3);cl.addStretch() |
| sc.setWidget(con) |
| l=QVBoxLayout(self);l.setContentsMargins(0,0,0,0);l.addWidget(sc) |
| self.refresh() |
| def refresh(self): |
| self._ws=[] |
| for fn,row in [(TMDB.trending,self._r1),(TMDB.movies,self._r2),(TMDB.tv,self._r3)]: |
| w=W(fn);w.done.connect(lambda d,r=row:r.set_items(d.get('results',[]) if d else [])) |
| w.start();self._ws.append(w) |
|
|
| |
| class Search(QWidget): |
| open_item=pyqtSignal(dict) |
| def __init__(self): |
| super().__init__() |
| l=QVBoxLayout(self);l.setContentsMargins(16,16,16,16) |
| self._inp=QLineEdit();self._inp.setPlaceholderText("Search movies & TV...");self._inp.returnPressed.connect(self._go) |
| l.addWidget(self._inp) |
| self._row=Row("Results");self._row.card_clicked.connect(self.open_item);l.addWidget(self._row);l.addStretch() |
| self._w=None |
| def _go(self): |
| q=self._inp.text().strip() |
| if q: |
| self._w=W(TMDB.search,q) |
| self._w.done.connect(lambda d:self._row.set_items([r for r in(d.get('results',[]) if d else[]) if r.get('poster_path') and r.get('media_type') in('movie','tv')])) |
| self._w.start() |
|
|
| |
| class Player(QWidget): |
| back=pyqtSignal() |
| def __init__(self): |
| super().__init__() |
| self._item=None;self._source_idx=0;self._tid=0 |
| |
| layout=QVBoxLayout(self);layout.setContentsMargins(12,12,12,12);layout.setSpacing(8) |
| |
| |
| top=QHBoxLayout() |
| bb=QPushButton("β Back");bb.clicked.connect(self.back.emit);top.addWidget(bb) |
| top.addStretch() |
| self._title_lbl=QLabel("...");self._title_lbl.setFont(QFont("Segoe UI",16,QFont.Weight.Bold));self._title_lbl.setStyleSheet("color:#fff") |
| top.addWidget(self._title_lbl);top.addStretch() |
| layout.addLayout(top) |
| |
| |
| src_row=QHBoxLayout() |
| src_row.addWidget(QLabel("Source:")) |
| self._src_btns=[] |
| for i,(name,_,_) in enumerate(SOURCES): |
| b=QPushButton(name);b.setObjectName("src") |
| b.clicked.connect(lambda _,idx=i:self._set_source(idx)) |
| src_row.addWidget(b);self._src_btns.append(b) |
| src_row.addStretch() |
| layout.addLayout(src_row) |
| |
| |
| self._ep_widget=QWidget() |
| ep_row=QHBoxLayout(self._ep_widget);ep_row.setContentsMargins(0,0,0,0) |
| ep_row.addWidget(QLabel("Season:")) |
| self._s_combo=QComboBox();self._s_combo.setFixedWidth(100) |
| self._s_combo.currentIndexChanged.connect(self._on_season_change) |
| ep_row.addWidget(self._s_combo) |
| ep_row.addWidget(QLabel("Episode:")) |
| self._e_combo=QComboBox();self._e_combo.setFixedWidth(250) |
| ep_row.addWidget(self._e_combo) |
| pb=QPushButton("βΆ Play Episode");pb.setObjectName("play") |
| pb.clicked.connect(self._play_current_ep) |
| ep_row.addWidget(pb) |
| ep_row.addStretch() |
| layout.addWidget(self._ep_widget) |
| |
| |
| self._movie_btn=QPushButton("βΆ Play Movie");self._movie_btn.setObjectName("play") |
| self._movie_btn.clicked.connect(self._play_current_movie) |
| layout.addWidget(self._movie_btn) |
| |
| |
| self._web=QWebEngineView() |
| page=QWebEnginePage(self._web);page.setBackgroundColor(QColor("#0a0a14")) |
| self._web.setPage(page) |
| s=self._web.settings() |
| s.setAttribute(QWebEngineSettings.WebAttribute.JavascriptEnabled,True) |
| s.setAttribute(QWebEngineSettings.WebAttribute.PluginsEnabled,True) |
| s.setAttribute(QWebEngineSettings.WebAttribute.FullScreenSupportEnabled,True) |
| self._web.setMinimumHeight(450) |
| self._web.setHtml("<html><body style='background:#0a0a14;color:#666;display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif'><p>Select a source and press Play</p></body></html>") |
| layout.addWidget(self._web) |
| |
| def load(self,item): |
| self._item=item;self._source_idx=0 |
| is_movie='title' in item |
| self._tid=item.get('id') |
| title=item.get('title') or item.get('name') or "?" |
| self._title_lbl.setText(title) |
| |
| |
| self._ep_widget.setVisible(not is_movie) |
| self._movie_btn.setVisible(is_movie) |
| |
| if not is_movie: |
| |
| self._s_combo.blockSignals(True) |
| self._s_combo.clear() |
| self._s_combo.blockSignals(False) |
| self._e_combo.clear() |
| self._sw=W(TMDB.tv_details,self._tid) |
| self._sw.done.connect(self._on_seasons) |
| self._sw.start() |
| else: |
| |
| QTimer.singleShot(300, self._play_current_movie) |
| |
| def _on_seasons(self,data): |
| if not data:return |
| ns=data.get('number_of_seasons',1) |
| self._s_combo.blockSignals(True) |
| for i in range(1,ns+1): |
| self._s_combo.addItem(f"Season {i}",i) |
| self._s_combo.blockSignals(False) |
| |
| self._on_season_change() |
| |
| def _on_season_change(self): |
| sn=self._s_combo.currentData() |
| if not sn:return |
| self._ew=W(TMDB.season,self._tid,sn) |
| self._ew.done.connect(self._on_episodes) |
| self._ew.start() |
| |
| def _on_episodes(self,data): |
| self._e_combo.clear() |
| if not data:return |
| for ep in data.get('episodes',[]): |
| num=ep.get('episode_number',0) |
| name=ep.get('name','') |
| self._e_combo.addItem(f"E{num}: {name[:35]}",num) |
| |
| def _play_current_movie(self): |
| _,movie_url,_=SOURCES[self._source_idx%len(SOURCES)] |
| url=movie_url.format(id=self._tid) |
| webbrowser.open(url) |
| self._web.setHtml(f"<html><body style='background:#0a0a14;color:#0f8;display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;flex-direction:column'><h2>Opened in browser</h2><p style='color:#888'>Playing from: {SOURCES[self._source_idx%len(SOURCES)][0]}</p><p style='color:#555'>If it didn't work, try another source button above</p></body></html>") |
| |
| def _play_current_ep(self): |
| sn=self._s_combo.currentData() or 1 |
| ep=self._e_combo.currentData() or 1 |
| _,_,tv_url=SOURCES[self._source_idx%len(SOURCES)] |
| url=tv_url.format(id=self._tid,s=sn,e=ep) |
| webbrowser.open(url) |
| self._web.setHtml(f"<html><body style='background:#0a0a14;color:#0f8;display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;flex-direction:column'><h2>Opened in browser</h2><p style='color:#888'>S{sn}E{ep} from: {SOURCES[self._source_idx%len(SOURCES)][0]}</p><p style='color:#555'>If it didn't work, try another source button above</p></body></html>") |
| |
| def _set_source(self,idx): |
| self._source_idx=idx |
| if self._item: |
| if 'title' in self._item: |
| self._play_current_movie() |
| else: |
| self._play_current_ep() |
|
|
| |
| class Lib(QWidget): |
| open_item=pyqtSignal(dict) |
| def __init__(self): |
| super().__init__() |
| l=QVBoxLayout(self);l.setContentsMargins(16,16,16,16) |
| self._wl=Row("π Watchlist");self._wl.card_clicked.connect(self.open_item);l.addWidget(self._wl) |
| self._hist=Row("π History");self._hist.card_clicked.connect(self.open_item);l.addWidget(self._hist) |
| l.addStretch() |
| def refresh(self): |
| d=lib_load() |
| self._wl.set_items(d.get('wl',[])) |
| self._hist.set_items(d.get('hist',[])) |
|
|
| |
| class KeyDialog(QDialog): |
| def __init__(self,parent=None): |
| super().__init__(parent) |
| self.setWindowTitle("Setup");self.setFixedSize(450,220) |
| self.setStyleSheet(f"QDialog{{background:#0a0a14}}QLabel{{color:#ddd}}QLineEdit{{background:#101020;border:1px solid #222;border-radius:6px;padding:10px;color:#eee}}QPushButton{{background:#00bfa5;color:#000;border:none;border-radius:6px;padding:10px 20px;font-weight:bold}}") |
| l=QVBoxLayout(self);l.setSpacing(12);l.setContentsMargins(24,24,24,24) |
| l.addWidget(QLabel("Enter your free TMDB API key:")) |
| l.addWidget(QLabel("Get one at: themoviedb.org/settings/api")) |
| self._inp=QLineEdit();self._inp.setPlaceholderText("Paste API key here...");l.addWidget(self._inp) |
| b=QPushButton("Save & Start");b.clicked.connect(self._save);l.addWidget(b) |
| def _save(self): |
| k=self._inp.text().strip() |
| if len(k)>10:set_key(k);self.accept() |
|
|
| |
| class Main(QMainWindow): |
| def __init__(self): |
| super().__init__() |
| self.setWindowTitle(APP);self.resize(1200,750);self.setStyleSheet(S) |
| |
| ct=QWidget();self.setCentralWidget(ct) |
| ml=QVBoxLayout(ct);ml.setContentsMargins(0,0,0,0);ml.setSpacing(0) |
| |
| |
| hdr=QWidget();hdr.setFixedHeight(50);hdr.setStyleSheet("background:#0c0c1a;border-bottom:1px solid #1a1a30") |
| hl=QHBoxLayout(hdr);hl.setContentsMargins(16,0,16,0) |
| |
| logo=QLabel("π¦ PeacocksHouse") |
| logo.setFont(QFont("Segoe UI",15,QFont.Weight.Bold)) |
| logo.setStyleSheet("color:#00bfa5") |
| hl.addWidget(logo) |
| hl.addSpacing(20) |
| |
| self._navs={} |
| for name in["Home","Search","Library"]: |
| b=QPushButton(name) |
| b.setStyleSheet("QPushButton{background:transparent;border:none;color:#666;font-size:12px;font-weight:bold;padding:6px 12px}QPushButton:hover{color:#fff}") |
| b.clicked.connect(lambda _,n=name:self._nav(n)) |
| hl.addWidget(b);self._navs[name]=b |
| hl.addStretch() |
| ml.addWidget(hdr) |
| |
| |
| self._stack=QStackedWidget() |
| self._home=Home();self._home.open_item.connect(self._open) |
| self._search=Search();self._search.open_item.connect(self._open) |
| self._lib=Lib();self._lib.open_item.connect(self._open) |
| self._player=Player();self._player.back.connect(lambda:self._stack.setCurrentIndex(0)) |
| self._stack.addWidget(self._home);self._stack.addWidget(self._search) |
| self._stack.addWidget(self._lib);self._stack.addWidget(self._player) |
| ml.addWidget(self._stack) |
| self._nav("Home") |
| |
| def _nav(self,name): |
| idx={"Home":0,"Search":1,"Library":2}.get(name,0) |
| self._stack.setCurrentIndex(idx) |
| if name=="Library":self._lib.refresh() |
| |
| def _open(self,item): |
| |
| d=lib_load() |
| if 'hist' not in d: d['hist']=[] |
| if 'wl' not in d: d['wl']=[] |
| entry={"id":item.get('id'),"title":item.get('title') or item.get('name'),"poster_path":item.get('poster_path'),"vote_average":item.get('vote_average',0)} |
| if 'title' not in item: entry['name']=item.get('name') |
| d['hist']=[e for e in d['hist'] if e.get('id')!=item.get('id')] |
| d['hist'].insert(0,entry) |
| d['hist']=d['hist'][:100] |
| lib_save(d) |
| |
| self._player.load(item) |
| self._stack.setCurrentIndex(3) |
|
|
| |
| def main(): |
| app=QApplication(sys.argv) |
| app.setFont(QFont("Segoe UI",10)) |
| p=QPalette();p.setColor(QPalette.ColorRole.Window,QColor("#0a0a14"));p.setColor(QPalette.ColorRole.Base,QColor("#0a0a14")) |
| p.setColor(QPalette.ColorRole.Text,QColor("#ddd"));p.setColor(QPalette.ColorRole.WindowText,QColor("#ddd")) |
| app.setPalette(p) |
| if not get_key(): |
| if KeyDialog().exec()!=QDialog.DialogCode.Accepted:sys.exit(0) |
| w=Main();w.show() |
| sys.exit(app.exec()) |
|
|
| if __name__=="__main__": |
| main() |
|
|