rpirate 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rpirate-0.1.0/MANIFEST.in +3 -0
- rpirate-0.1.0/PKG-INFO +56 -0
- rpirate-0.1.0/README.md +40 -0
- rpirate-0.1.0/pyproject.toml +28 -0
- rpirate-0.1.0/setup.cfg +4 -0
- rpirate-0.1.0/src/pirate_three/__init__.py +1 -0
- rpirate-0.1.0/src/pirate_three/config.py +33 -0
- rpirate-0.1.0/src/pirate_three/default_config.yaml +24 -0
- rpirate-0.1.0/src/pirate_three/logger.py +39 -0
- rpirate-0.1.0/src/pirate_three/main.py +216 -0
- rpirate-0.1.0/src/pirate_three/overlay.py +144 -0
- rpirate-0.1.0/src/pirate_three/player.py +77 -0
- rpirate-0.1.0/src/pirate_three/static/downloads.js +111 -0
- rpirate-0.1.0/src/pirate_three/static/favicon.ico +0 -0
- rpirate-0.1.0/src/pirate_three/static/home.js +207 -0
- rpirate-0.1.0/src/pirate_three/static/styles.css +344 -0
- rpirate-0.1.0/src/pirate_three/static/toast.js +19 -0
- rpirate-0.1.0/src/pirate_three/templates/base.html +86 -0
- rpirate-0.1.0/src/pirate_three/templates/controller.html +104 -0
- rpirate-0.1.0/src/pirate_three/templates/downloads.html +17 -0
- rpirate-0.1.0/src/pirate_three/templates/index.html +30 -0
- rpirate-0.1.0/src/pirate_three/torrent.py +233 -0
- rpirate-0.1.0/src/rpirate.egg-info/PKG-INFO +56 -0
- rpirate-0.1.0/src/rpirate.egg-info/SOURCES.txt +26 -0
- rpirate-0.1.0/src/rpirate.egg-info/dependency_links.txt +1 -0
- rpirate-0.1.0/src/rpirate.egg-info/entry_points.txt +2 -0
- rpirate-0.1.0/src/rpirate.egg-info/requires.txt +5 -0
- rpirate-0.1.0/src/rpirate.egg-info/top_level.txt +1 -0
rpirate-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rpirate
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A full stack torrent streaming app to get you free movies.
|
|
5
|
+
Author: Miles Hilliard
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: Flask
|
|
12
|
+
Requires-Dist: python-vlc
|
|
13
|
+
Requires-Dist: PyYAML
|
|
14
|
+
Requires-Dist: requests
|
|
15
|
+
Requires-Dist: tpblite
|
|
16
|
+
|
|
17
|
+
# PiRate v3.0
|
|
18
|
+
|
|
19
|
+
Stream your favorite movies and TV shows for free.
|
|
20
|
+
|
|
21
|
+
PiRate is a modern, mobile-friendly web interface that wraps The Pirate Bay (TPB) and the qBittorrent API to provide a seamless, Netflix-like streaming experience from your couch.
|
|
22
|
+
|
|
23
|
+
> **Disclaimer:** This software is an interface for third-party APIs and torrent sites. The creator of this software is not responsible for any content downloaded, hosted, or distributed. Please use a VPN and use responsibly in accordance with your local laws.
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- **Cinematic UI:** Beautiful, responsive dark-mode interface optimized for mobile and desktop.
|
|
28
|
+
- **Dynamic Content:** Browse trending and genre-specific movies fetched directly via the YTS API.
|
|
29
|
+
- **Virtual Remote:** Control playback directly from your smartphone with a sleek web remote.
|
|
30
|
+
- **Live Downloads:** Monitor and manage your active torrents with real-time progress.
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
### 1. Prerequisites
|
|
35
|
+
Ensure you have the following installed:
|
|
36
|
+
- [Python 3](https://www.python.org/downloads/)
|
|
37
|
+
- [VLC Media Player](https://www.videolan.org/vlc/)
|
|
38
|
+
- [qBittorrent](https://www.qbittorrent.org/)
|
|
39
|
+
- A reliable VPN (e.g., [Proton VPN](https://protonvpn.com))
|
|
40
|
+
|
|
41
|
+
### 2. Installation
|
|
42
|
+
```sh
|
|
43
|
+
git clone https://github.com/syntaxerror019/pirate-three.git
|
|
44
|
+
cd pirate-three
|
|
45
|
+
pip install -r requirements.txt
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 3. Usage
|
|
49
|
+
Make sure qBittorrent is running in the background, then start the PiRate server:
|
|
50
|
+
```sh
|
|
51
|
+
python3 main.py
|
|
52
|
+
```
|
|
53
|
+
Open your browser and navigate to `http://localhost:3000`.
|
|
54
|
+
|
|
55
|
+
## Contributing
|
|
56
|
+
Issues and Pull Requests are always welcome!
|
rpirate-0.1.0/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# PiRate v3.0
|
|
2
|
+
|
|
3
|
+
Stream your favorite movies and TV shows for free.
|
|
4
|
+
|
|
5
|
+
PiRate is a modern, mobile-friendly web interface that wraps The Pirate Bay (TPB) and the qBittorrent API to provide a seamless, Netflix-like streaming experience from your couch.
|
|
6
|
+
|
|
7
|
+
> **Disclaimer:** This software is an interface for third-party APIs and torrent sites. The creator of this software is not responsible for any content downloaded, hosted, or distributed. Please use a VPN and use responsibly in accordance with your local laws.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Cinematic UI:** Beautiful, responsive dark-mode interface optimized for mobile and desktop.
|
|
12
|
+
- **Dynamic Content:** Browse trending and genre-specific movies fetched directly via the YTS API.
|
|
13
|
+
- **Virtual Remote:** Control playback directly from your smartphone with a sleek web remote.
|
|
14
|
+
- **Live Downloads:** Monitor and manage your active torrents with real-time progress.
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### 1. Prerequisites
|
|
19
|
+
Ensure you have the following installed:
|
|
20
|
+
- [Python 3](https://www.python.org/downloads/)
|
|
21
|
+
- [VLC Media Player](https://www.videolan.org/vlc/)
|
|
22
|
+
- [qBittorrent](https://www.qbittorrent.org/)
|
|
23
|
+
- A reliable VPN (e.g., [Proton VPN](https://protonvpn.com))
|
|
24
|
+
|
|
25
|
+
### 2. Installation
|
|
26
|
+
```sh
|
|
27
|
+
git clone https://github.com/syntaxerror019/pirate-three.git
|
|
28
|
+
cd pirate-three
|
|
29
|
+
pip install -r requirements.txt
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 3. Usage
|
|
33
|
+
Make sure qBittorrent is running in the background, then start the PiRate server:
|
|
34
|
+
```sh
|
|
35
|
+
python3 main.py
|
|
36
|
+
```
|
|
37
|
+
Open your browser and navigate to `http://localhost:3000`.
|
|
38
|
+
|
|
39
|
+
## Contributing
|
|
40
|
+
Issues and Pull Requests are always welcome!
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rpirate"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Miles Hilliard" },
|
|
10
|
+
]
|
|
11
|
+
description = "A full stack torrent streaming app to get you free movies."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.7"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"Flask",
|
|
21
|
+
"python-vlc",
|
|
22
|
+
"PyYAML",
|
|
23
|
+
"requests",
|
|
24
|
+
"tpblite"
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
pirate = "pirate_three:main"
|
rpirate-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .main import main
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
|
|
5
|
+
class Config():
|
|
6
|
+
def __init__(self, config_file="config.yaml"):
|
|
7
|
+
self.config_file = config_file
|
|
8
|
+
self.ensure_config()
|
|
9
|
+
self.load_config()
|
|
10
|
+
|
|
11
|
+
def ensure_config(self):
|
|
12
|
+
"""Ensure the config file exists in the current working directory, otherwise copy default."""
|
|
13
|
+
if not os.path.exists(self.config_file):
|
|
14
|
+
print(f"[{self.config_file}] not found in current directory. Creating default configuration.")
|
|
15
|
+
|
|
16
|
+
# Find default_config.yaml in the package directory
|
|
17
|
+
pkg_dir = os.path.dirname(os.path.abspath(__file__))
|
|
18
|
+
default_config_path = os.path.join(pkg_dir, "default_config.yaml")
|
|
19
|
+
|
|
20
|
+
if os.path.exists(default_config_path):
|
|
21
|
+
shutil.copy(default_config_path, self.config_file)
|
|
22
|
+
else:
|
|
23
|
+
print("Error: Package default_config.yaml is missing!")
|
|
24
|
+
|
|
25
|
+
def load_config(self):
|
|
26
|
+
if os.path.exists(self.config_file):
|
|
27
|
+
with open(self.config_file, 'r') as file:
|
|
28
|
+
self.config = yaml.safe_load(file) or {}
|
|
29
|
+
else:
|
|
30
|
+
self.config = {}
|
|
31
|
+
|
|
32
|
+
def get(self, key, default=None):
|
|
33
|
+
return self.config.get(key, default)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
############### www.mileshilliard.com ###################
|
|
2
|
+
#
|
|
3
|
+
# User Configuration File.
|
|
4
|
+
#
|
|
5
|
+
# Adjust settings per your taste and preference!
|
|
6
|
+
#
|
|
7
|
+
# The default settings should work fine,
|
|
8
|
+
# so there is no immediate need to touch this.
|
|
9
|
+
#
|
|
10
|
+
#########################################################
|
|
11
|
+
|
|
12
|
+
download_folder: /home/miles/Downloads/pirate-downloads
|
|
13
|
+
create_folder: true
|
|
14
|
+
|
|
15
|
+
# Network
|
|
16
|
+
|
|
17
|
+
tbp_url: https://thepiratebay7.com/
|
|
18
|
+
host: 0.0.0.0
|
|
19
|
+
port: 3000
|
|
20
|
+
|
|
21
|
+
# QBitTorrent
|
|
22
|
+
|
|
23
|
+
qb_url: http://localhost:8080
|
|
24
|
+
use_old_api: false # Leave off unless you have an old version of qBittorrent
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
# ANSI escape codes
|
|
5
|
+
LOG_COLORS = {
|
|
6
|
+
"DEBUG": "\033[36m", # Cyan
|
|
7
|
+
"INFO": "\033[32m", # Green
|
|
8
|
+
"WARNING": "\033[33m", # Yellow
|
|
9
|
+
"ERROR": "\033[31m", # Red
|
|
10
|
+
"CRITICAL": "\033[35m", # Magenta
|
|
11
|
+
"RESET": "\033[0m" # Reset to default
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class ColoredFormatter(logging.Formatter):
|
|
15
|
+
def format(self, record):
|
|
16
|
+
log_color = LOG_COLORS.get(record.levelname, LOG_COLORS["RESET"])
|
|
17
|
+
record.msg = f"{log_color}{record.msg}{LOG_COLORS['RESET']}"
|
|
18
|
+
return super().format(record)
|
|
19
|
+
|
|
20
|
+
logging.basicConfig(
|
|
21
|
+
level=logging.DEBUG,
|
|
22
|
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
23
|
+
handlers=[
|
|
24
|
+
logging.FileHandler("app.log"), # Log to a file (no colors)
|
|
25
|
+
logging.StreamHandler(sys.stdout) # log to console
|
|
26
|
+
]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# colored formatting to console logs only
|
|
30
|
+
console_handler = logging.getLogger().handlers[1] # Second handler (StreamHandler)
|
|
31
|
+
console_handler.setFormatter(ColoredFormatter("%(asctime)s - %(levelname)s - %(message)s"))
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
# Test the logger
|
|
35
|
+
logging.debug("This is a debug message")
|
|
36
|
+
logging.info("This is an info message")
|
|
37
|
+
logging.warning("This is a warning")
|
|
38
|
+
logging.error("This is an error")
|
|
39
|
+
logging.critical("This is a critical error")
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
from flask import Flask, request, jsonify, render_template
|
|
2
|
+
from tpblite import TPB
|
|
3
|
+
import subprocess
|
|
4
|
+
import socket
|
|
5
|
+
import urllib.request
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from .player import Player
|
|
11
|
+
from .torrent import tr
|
|
12
|
+
from .config import Config
|
|
13
|
+
|
|
14
|
+
# Init Flask with explicit template/static folder paths so it works when installed via pip
|
|
15
|
+
pkg_dir = os.path.dirname(os.path.abspath(__file__))
|
|
16
|
+
template_dir = os.path.join(pkg_dir, "templates")
|
|
17
|
+
static_dir = os.path.join(pkg_dir, "static")
|
|
18
|
+
|
|
19
|
+
app = Flask(__name__, template_folder=template_dir, static_folder=static_dir)
|
|
20
|
+
|
|
21
|
+
cf = Config("config.yaml")
|
|
22
|
+
|
|
23
|
+
# Init Pirate Bay
|
|
24
|
+
tpb = TPB(base_url=cf.get("tbp_url"))
|
|
25
|
+
|
|
26
|
+
player = Player()
|
|
27
|
+
|
|
28
|
+
# Init qBittorrent wrapper (API v4 by default)
|
|
29
|
+
qb = tr(url=cf.get("qb_url")) # qBittorrent default WebUI port is 8080
|
|
30
|
+
|
|
31
|
+
def get_local_ip():
|
|
32
|
+
try:
|
|
33
|
+
# Use hostname -I to get all IPs and prioritize the typical home Wi-Fi range
|
|
34
|
+
output = subprocess.check_output(['hostname', '-I'], text=True).strip()
|
|
35
|
+
ips = output.split()
|
|
36
|
+
|
|
37
|
+
# 1. Highest priority: 192.168.x.x (standard home router)
|
|
38
|
+
for ip in ips:
|
|
39
|
+
if ip.startswith("192.168."):
|
|
40
|
+
return ip
|
|
41
|
+
|
|
42
|
+
# 2. Secondary priority: other local subnets excluding common VPN/Docker defaults
|
|
43
|
+
for ip in ips:
|
|
44
|
+
if (ip.startswith("10.") and not ip.startswith("10.2.")) or \
|
|
45
|
+
(ip.startswith("172.") and not ip.startswith("172.17.")):
|
|
46
|
+
return ip
|
|
47
|
+
|
|
48
|
+
# 3. Fallback to the first valid IPv4 that isn't localhost
|
|
49
|
+
for ip in ips:
|
|
50
|
+
if "." in ip and not ip.startswith("127."):
|
|
51
|
+
return ip
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
# Last resort fallback: dummy UDP socket
|
|
57
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
58
|
+
s.settimeout(0.5)
|
|
59
|
+
s.connect(("8.8.8.8", 80))
|
|
60
|
+
ip = s.getsockname()[0]
|
|
61
|
+
s.close()
|
|
62
|
+
return ip
|
|
63
|
+
except:
|
|
64
|
+
return "127.0.0.1"
|
|
65
|
+
|
|
66
|
+
@app.route("/")
|
|
67
|
+
def index():
|
|
68
|
+
return render_template("index.html")
|
|
69
|
+
|
|
70
|
+
@app.route("/mydownloads")
|
|
71
|
+
def downloads_page():
|
|
72
|
+
return render_template("downloads.html")
|
|
73
|
+
|
|
74
|
+
@app.route("/controller")
|
|
75
|
+
def remote():
|
|
76
|
+
return render_template("controller.html")
|
|
77
|
+
|
|
78
|
+
@app.route("/api/movies")
|
|
79
|
+
def api_movies():
|
|
80
|
+
qs = request.query_string.decode('utf-8')
|
|
81
|
+
url = f"https://yts.lt/api/v2/list_movies.json?{qs}"
|
|
82
|
+
try:
|
|
83
|
+
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
|
84
|
+
with urllib.request.urlopen(req, timeout=10) as response:
|
|
85
|
+
data = json.loads(response.read().decode('utf-8'))
|
|
86
|
+
return jsonify(data)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
return jsonify({"status": "error", "message": str(e)}), 500
|
|
89
|
+
|
|
90
|
+
@app.route("/search")
|
|
91
|
+
def search():
|
|
92
|
+
query = request.args.get("q", "")
|
|
93
|
+
if not query:
|
|
94
|
+
return jsonify([])
|
|
95
|
+
torrents = tpb.search(query)
|
|
96
|
+
results = [{
|
|
97
|
+
"title": t.title,
|
|
98
|
+
"seeds": t.seeds,
|
|
99
|
+
"leeches": t.leeches,
|
|
100
|
+
"upload_date": t.upload_date,
|
|
101
|
+
"uploader": t.uploader,
|
|
102
|
+
"filesize": t.filesize,
|
|
103
|
+
"byte_size": t.byte_size,
|
|
104
|
+
"magnetlink": t.magnetlink,
|
|
105
|
+
"url": t.url,
|
|
106
|
+
"is_trusted": t.is_trusted,
|
|
107
|
+
"is_vip": t.is_vip,
|
|
108
|
+
"infohash": t.infohash,
|
|
109
|
+
"category": t.category
|
|
110
|
+
} for t in torrents]
|
|
111
|
+
return jsonify(results)
|
|
112
|
+
|
|
113
|
+
@app.route("/download", methods=["POST"])
|
|
114
|
+
def download():
|
|
115
|
+
data = request.get_json()
|
|
116
|
+
magnet = data.get("magnetlink")
|
|
117
|
+
if not magnet:
|
|
118
|
+
return jsonify({"error": "No magnetlink"}), 400
|
|
119
|
+
|
|
120
|
+
success = qb.download_torrent(magnet)
|
|
121
|
+
if success:
|
|
122
|
+
return jsonify({"status": "added"})
|
|
123
|
+
return jsonify({"status": "failed"}), 500
|
|
124
|
+
|
|
125
|
+
@app.route("/downloads")
|
|
126
|
+
def downloads():
|
|
127
|
+
try:
|
|
128
|
+
tasks = qb.torrent_status()
|
|
129
|
+
except Exception as e:
|
|
130
|
+
return jsonify({"error": str(e)}), 500
|
|
131
|
+
|
|
132
|
+
results = [{
|
|
133
|
+
"hash": d["hash"],
|
|
134
|
+
"name": d["name"],
|
|
135
|
+
"status": d["state"],
|
|
136
|
+
"progress": f"{d['progress']*100:.2f}%",
|
|
137
|
+
"speed": f"{d['dlspeed']/1024:.1f} KiB/s",
|
|
138
|
+
"eta": tr.format_eta(d["eta"])
|
|
139
|
+
} for d in tasks]
|
|
140
|
+
return jsonify(results)
|
|
141
|
+
|
|
142
|
+
@app.route('/watch/<name>')
|
|
143
|
+
def watch(name):
|
|
144
|
+
file = qb.get_file_path(name)
|
|
145
|
+
if not file:
|
|
146
|
+
return jsonify({"error": "File not found"}), 404
|
|
147
|
+
|
|
148
|
+
player.set_media(file)
|
|
149
|
+
player.play()
|
|
150
|
+
|
|
151
|
+
return jsonify({'error': None, 'file': file}), 200
|
|
152
|
+
|
|
153
|
+
@app.route('/remove/<hash>', methods=['DELETE'])
|
|
154
|
+
def remove(hash):
|
|
155
|
+
success = qb.delete(hash)
|
|
156
|
+
if success:
|
|
157
|
+
return jsonify({'error': None}), 200
|
|
158
|
+
return jsonify({'error': 'Failed to remove torrent'}), 500
|
|
159
|
+
|
|
160
|
+
@app.route("/pause/<hash>", methods=["POST"])
|
|
161
|
+
def pause(hash):
|
|
162
|
+
if not hash:
|
|
163
|
+
return jsonify({"error": "No hash"}), 400
|
|
164
|
+
qb.pause(hash)
|
|
165
|
+
return jsonify({"status": "paused"})
|
|
166
|
+
|
|
167
|
+
@app.route("/resume/<hash>", methods=["POST"])
|
|
168
|
+
def resume(hash):
|
|
169
|
+
if not hash:
|
|
170
|
+
return jsonify({"error": "No hash"}), 400
|
|
171
|
+
qb.resume(hash)
|
|
172
|
+
return jsonify({"status": "resumed"})
|
|
173
|
+
|
|
174
|
+
@app.route('/command/<cmd>', methods=['POST'])
|
|
175
|
+
def command(cmd):
|
|
176
|
+
if cmd == "plp":
|
|
177
|
+
player.pause()
|
|
178
|
+
if cmd == "stp":
|
|
179
|
+
print("STOPPING")
|
|
180
|
+
player.stop_and_close()
|
|
181
|
+
if cmd == "rrw":
|
|
182
|
+
player.rewind(60)
|
|
183
|
+
if cmd == "ffw":
|
|
184
|
+
player.fast_forward(60)
|
|
185
|
+
if cmd == "ccy":
|
|
186
|
+
player.enable_subtitles()
|
|
187
|
+
if cmd == "ccn":
|
|
188
|
+
player.disable_subtitles()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
return jsonify({"status": "success"})
|
|
192
|
+
|
|
193
|
+
def setup():
|
|
194
|
+
qb.set_torrent_download_location(cf.get("download_folder"), create=cf.get("create_folder", True))
|
|
195
|
+
|
|
196
|
+
if not qb.check_connection():
|
|
197
|
+
raise Exception("Failed to connect to QBittorrent. Please check your settings and ensure QBittorrent is running.")
|
|
198
|
+
|
|
199
|
+
def main():
|
|
200
|
+
setup()
|
|
201
|
+
|
|
202
|
+
port = cf.get("port")
|
|
203
|
+
if not port:
|
|
204
|
+
port = 3000
|
|
205
|
+
|
|
206
|
+
app_url = f"http://{get_local_ip()}:{port}"
|
|
207
|
+
print(f"Starting overlay with URL: {app_url}")
|
|
208
|
+
|
|
209
|
+
# Calculate the path to the overlay script dynamically so it works anywhere
|
|
210
|
+
overlay_path = os.path.join(pkg_dir, "overlay.py")
|
|
211
|
+
subprocess.Popen([sys.executable, overlay_path, app_url])
|
|
212
|
+
|
|
213
|
+
app.run(debug=False, host=cf.get("host"), port=port)
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
main()
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import tkinter as tk
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
import math
|
|
6
|
+
import random
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
def get_ip_from_args():
|
|
10
|
+
if len(sys.argv) > 1:
|
|
11
|
+
return sys.argv[1]
|
|
12
|
+
return "No IP Provided"
|
|
13
|
+
|
|
14
|
+
class IPOverlay:
|
|
15
|
+
def __init__(self, root, ip_text):
|
|
16
|
+
self.root = root
|
|
17
|
+
self.ip_text = ip_text
|
|
18
|
+
|
|
19
|
+
# Configure window
|
|
20
|
+
root.title("PiRate Idle Screen")
|
|
21
|
+
root.geometry(f"{root.winfo_screenwidth()}x{root.winfo_screenheight()}+0+0")
|
|
22
|
+
root.overrideredirect(True)
|
|
23
|
+
root.attributes("-topmost", True)
|
|
24
|
+
root.configure(bg="#050505")
|
|
25
|
+
try:
|
|
26
|
+
root.attributes("-fullscreen", True)
|
|
27
|
+
except:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
root.config(cursor="none") # Hide mouse cursor
|
|
31
|
+
|
|
32
|
+
self.w = root.winfo_screenwidth()
|
|
33
|
+
self.h = root.winfo_screenheight()
|
|
34
|
+
|
|
35
|
+
# Main canvas for drawing
|
|
36
|
+
self.canvas = tk.Canvas(root, bg="#050505", highlightthickness=0, width=self.w, height=self.h)
|
|
37
|
+
self.canvas.pack(fill="both", expand=True)
|
|
38
|
+
|
|
39
|
+
# Create glowing orbs (cinematic Apple TV style)
|
|
40
|
+
self.orbs = []
|
|
41
|
+
# Dark, subtle ambient colors
|
|
42
|
+
colors = ["#1a0f2e", "#0f172a", "#2e0f1a"]
|
|
43
|
+
for i, color in enumerate(colors):
|
|
44
|
+
size = self.h * 1.5
|
|
45
|
+
x = self.w / 2
|
|
46
|
+
y = self.h / 2
|
|
47
|
+
orb = self.canvas.create_oval(x-size/2, y-size/2, x+size/2, y+size/2, fill=color, outline="")
|
|
48
|
+
self.orbs.append({"id": orb, "cx": x, "cy": y, "r": size/2, "offset": i*2.5})
|
|
49
|
+
|
|
50
|
+
# Create particles (stars/dust)
|
|
51
|
+
self.particles = []
|
|
52
|
+
for _ in range(80):
|
|
53
|
+
x = random.randint(0, self.w)
|
|
54
|
+
y = random.randint(0, self.h)
|
|
55
|
+
r = random.uniform(0.5, 1.8)
|
|
56
|
+
dx = random.uniform(-0.3, 0.3)
|
|
57
|
+
dy = random.uniform(-0.3, 0.3)
|
|
58
|
+
opacity = random.uniform(0.1, 0.6)
|
|
59
|
+
shade = int(255 * opacity)
|
|
60
|
+
color = f"#{shade:02x}{shade:02x}{shade:02x}"
|
|
61
|
+
p_id = self.canvas.create_oval(x-r, y-r, x+r, y+r, fill=color, outline="")
|
|
62
|
+
self.particles.append({"id": p_id, "x": x, "y": y, "dx": dx, "dy": dy, "r": r})
|
|
63
|
+
|
|
64
|
+
# Text fonts
|
|
65
|
+
font_time = ("Helvetica", max(72, int(self.h / 8)), "normal")
|
|
66
|
+
font_date = ("Helvetica", max(24, int(self.h / 30)), "normal")
|
|
67
|
+
font_large = ("Helvetica", max(48, int(self.h / 15)), "bold")
|
|
68
|
+
font_small = ("Helvetica", max(20, int(self.h / 35)))
|
|
69
|
+
|
|
70
|
+
# Time/Date overlay (top/center)
|
|
71
|
+
self.time_text = self.canvas.create_text(self.w/2, self.h/4, text="", font=font_time, fill="#ffffff")
|
|
72
|
+
self.date_text = self.canvas.create_text(self.w/2, self.h/4 + max(60, int(self.h/15)), text="", font=font_date, fill="#a1a1aa")
|
|
73
|
+
|
|
74
|
+
# Center text
|
|
75
|
+
self.title_text = self.canvas.create_text(self.w/2, self.h*0.65, text="PiRate Remote", font=font_large, fill="#ffffff")
|
|
76
|
+
self.url_text = self.canvas.create_text(self.w/2, self.h*0.65 + max(40, int(self.h/20)), text=f"Connect to: {self.ip_text}", font=font_small, fill="#38bdf8")
|
|
77
|
+
|
|
78
|
+
# Bindings to close
|
|
79
|
+
root.bind("<Key>", lambda e: root.destroy())
|
|
80
|
+
root.bind("<Button-1>", lambda e: root.destroy())
|
|
81
|
+
|
|
82
|
+
self._running = True
|
|
83
|
+
self._animate()
|
|
84
|
+
self._update_time()
|
|
85
|
+
|
|
86
|
+
def _update_time(self):
|
|
87
|
+
if not self._running:
|
|
88
|
+
return
|
|
89
|
+
now = datetime.now()
|
|
90
|
+
time_str = now.strftime("%I:%M %p").lstrip('0')
|
|
91
|
+
date_str = now.strftime("%A, %B %d")
|
|
92
|
+
|
|
93
|
+
self.canvas.itemconfig(self.time_text, text=time_str)
|
|
94
|
+
self.canvas.itemconfig(self.date_text, text=date_str)
|
|
95
|
+
|
|
96
|
+
# Bring all text to front
|
|
97
|
+
self.canvas.tag_raise(self.time_text)
|
|
98
|
+
self.canvas.tag_raise(self.date_text)
|
|
99
|
+
self.canvas.tag_raise(self.title_text)
|
|
100
|
+
self.canvas.tag_raise(self.url_text)
|
|
101
|
+
|
|
102
|
+
self.root.after(1000, self._update_time)
|
|
103
|
+
|
|
104
|
+
def _animate(self):
|
|
105
|
+
if not self._running:
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
t = time.time() * 0.15
|
|
109
|
+
|
|
110
|
+
# Slowly move the orbs around to create a breathing cinematic effect
|
|
111
|
+
for i, orb in enumerate(self.orbs):
|
|
112
|
+
dx = math.sin(t + orb["offset"]) * (self.w * 0.25)
|
|
113
|
+
dy = math.cos(t * 0.8 + orb["offset"]) * (self.h * 0.25)
|
|
114
|
+
|
|
115
|
+
x1 = orb["cx"] + dx - orb["r"]
|
|
116
|
+
y1 = orb["cy"] + dy - orb["r"]
|
|
117
|
+
x2 = orb["cx"] + dx + orb["r"]
|
|
118
|
+
y2 = orb["cy"] + dy + orb["r"]
|
|
119
|
+
|
|
120
|
+
self.canvas.coords(orb["id"], x1, y1, x2, y2)
|
|
121
|
+
|
|
122
|
+
# Move particles
|
|
123
|
+
for p in self.particles:
|
|
124
|
+
p["x"] += p["dx"]
|
|
125
|
+
p["y"] += p["dy"]
|
|
126
|
+
|
|
127
|
+
# wrap around
|
|
128
|
+
if p["x"] < 0: p["x"] = self.w
|
|
129
|
+
elif p["x"] > self.w: p["x"] = 0
|
|
130
|
+
if p["y"] < 0: p["y"] = self.h
|
|
131
|
+
elif p["y"] > self.h: p["y"] = 0
|
|
132
|
+
|
|
133
|
+
self.canvas.coords(p["id"], p["x"]-p["r"], p["y"]-p["r"], p["x"]+p["r"], p["y"]+p["r"])
|
|
134
|
+
|
|
135
|
+
self.root.after(40, self._animate)
|
|
136
|
+
|
|
137
|
+
def main():
|
|
138
|
+
ip = get_ip_from_args()
|
|
139
|
+
root = tk.Tk()
|
|
140
|
+
app = IPOverlay(root, ip)
|
|
141
|
+
root.mainloop()
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
main()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import vlc
|
|
2
|
+
import time
|
|
3
|
+
import os
|
|
4
|
+
from .logger import logging
|
|
5
|
+
|
|
6
|
+
class Player:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.media_path = None
|
|
9
|
+
self.instance = vlc.Instance()
|
|
10
|
+
self.player = self.instance.media_player_new()
|
|
11
|
+
|
|
12
|
+
def set_media(self, media_path):
|
|
13
|
+
self.media_path = media_path
|
|
14
|
+
media = self.instance.media_new(media_path)
|
|
15
|
+
self.player.set_media(media)
|
|
16
|
+
|
|
17
|
+
def play(self):
|
|
18
|
+
if not self.media_path:
|
|
19
|
+
logging.error("No media file set!")
|
|
20
|
+
return
|
|
21
|
+
self.player.set_fullscreen(True)
|
|
22
|
+
self.player.play()
|
|
23
|
+
self.player.video_set_spu(-1)
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
def pause(self):
|
|
27
|
+
if self.player.is_playing():
|
|
28
|
+
self.player.pause()
|
|
29
|
+
else:
|
|
30
|
+
self.player.play()
|
|
31
|
+
|
|
32
|
+
def fast_forward(self, seconds=10):
|
|
33
|
+
current_time = self.player.get_time()
|
|
34
|
+
self.player.set_time(current_time + (seconds * 1000))
|
|
35
|
+
|
|
36
|
+
def rewind(self, seconds=10):
|
|
37
|
+
current_time = self.player.get_time()
|
|
38
|
+
new_time = max(0, current_time - (seconds * 1000))
|
|
39
|
+
self.player.set_time(new_time)
|
|
40
|
+
|
|
41
|
+
def enable_subtitles(self):
|
|
42
|
+
spu_count = self.player.video_get_spu_count() # Get number of available subtitle tracks
|
|
43
|
+
logging.debug(f"Available subtitle tracks: {spu_count}")
|
|
44
|
+
|
|
45
|
+
if spu_count > 0:
|
|
46
|
+
spu_tracks = self.player.video_get_spu_description()
|
|
47
|
+
if len(spu_tracks) > 1:
|
|
48
|
+
second_track = spu_tracks[1][0] # Extract the second subtitle track ID
|
|
49
|
+
logging.debug(f"Enabling subtitles: Track {second_track}")
|
|
50
|
+
self.player.video_set_spu(second_track)
|
|
51
|
+
else:
|
|
52
|
+
logging.debug("Second subtitle track not available.")
|
|
53
|
+
else:
|
|
54
|
+
logging.debug("No subtitles available.")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def disable_subtitles(self):
|
|
58
|
+
self.player.video_set_spu(-1) # -1 disables subtitles
|
|
59
|
+
|
|
60
|
+
def stop_and_close(self):
|
|
61
|
+
self.player.stop()
|
|
62
|
+
|
|
63
|
+
def is_playing(self):
|
|
64
|
+
return self.player.is_playing()
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
player = Player()
|
|
68
|
+
player.set_media("example.mp4")
|
|
69
|
+
player.play()
|
|
70
|
+
time.sleep(5) # Let it play for 5 seconds
|
|
71
|
+
player.fast_forward(10) # Fast forward 10 seconds
|
|
72
|
+
time.sleep(5)
|
|
73
|
+
player.pause()
|
|
74
|
+
time.sleep(2)
|
|
75
|
+
player.play()
|
|
76
|
+
time.sleep(5)
|
|
77
|
+
player.stop_and_close()
|