#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 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)) # ━━━ TMDB ━━━ 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 "" # ━━━ STREAM SOURCES ━━━ 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 # ━━━ WORKERS ━━━ 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 # ━━━ STYLE ━━━ 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} """ # ━━━ CARD ━━━ 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) # Load image 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) # ━━━ ROW ━━━ 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) # ━━━ HOME ━━━ 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) # ━━━ SEARCH ━━━ 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() # ━━━ PLAYER PAGE ━━━ 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 bar (always visible) 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) # Source buttons 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) # Episode picker row (hidden for movies) 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) # Play movie button (hidden for TV) 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) # WebView (always exists, just changes URL) 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("

Select a source and press Play

") 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) # Show/hide episode picker vs movie button self._ep_widget.setVisible(not is_movie) self._movie_btn.setVisible(is_movie) if not is_movie: # Load seasons 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: # Auto-play movie 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) # Load first season episodes 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"

Opened in browser

Playing from: {SOURCES[self._source_idx%len(SOURCES)][0]}

If it didn't work, try another source button above

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

Opened in browser

S{sn}E{ep} from: {SOURCES[self._source_idx%len(SOURCES)][0]}

If it didn't work, try another source button above

") 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() # ━━━ LIBRARY ━━━ 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',[])) # ━━━ API KEY DIALOG ━━━ 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() # ━━━ MAIN WINDOW ━━━ 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) # Header 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) # Pages 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): # Save to history 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) # Show player self._player.load(item) self._stack.setCurrentIndex(3) # ━━━ ENTRY ━━━ 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()