MoneyPack-Security-Suite / peacockshouse.py
MoneyPack's picture
Opens in real browser now - no more embed issues, works 100%
60e7a18 verified
Raw
History Blame Contribute Delete
20.1 kB
#!/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("<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)
# 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"<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()
# ━━━ 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()