rtube 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.
- rtube-0.1.0/LICENSE +21 -0
- rtube-0.1.0/MANIFEST.in +6 -0
- rtube-0.1.0/PKG-INFO +25 -0
- rtube-0.1.0/README.md +364 -0
- rtube-0.1.0/migrations/env.py +113 -0
- rtube-0.1.0/migrations/versions/add_auth_type_to_users.py +60 -0
- rtube-0.1.0/migrations/versions/add_favorites_table.py +33 -0
- rtube-0.1.0/migrations/versions/add_playlists_tables.py +47 -0
- rtube-0.1.0/migrations/versions/add_sso_subject_to_users.py +56 -0
- rtube-0.1.0/migrations/versions/ce7c7ff2a0aa_initial_schema.py +94 -0
- rtube-0.1.0/pyproject.toml +53 -0
- rtube-0.1.0/rtube/__init__.py +4 -0
- rtube-0.1.0/rtube/__version__.py +1 -0
- rtube-0.1.0/rtube/app.py +338 -0
- rtube-0.1.0/rtube/models.py +133 -0
- rtube-0.1.0/rtube/models_auth.py +177 -0
- rtube-0.1.0/rtube/routes/__init__.py +4 -0
- rtube-0.1.0/rtube/routes/admin.py +266 -0
- rtube-0.1.0/rtube/routes/auth.py +435 -0
- rtube-0.1.0/rtube/routes/encoding.py +130 -0
- rtube-0.1.0/rtube/routes/playlists.py +215 -0
- rtube-0.1.0/rtube/routes/videos.py +551 -0
- rtube-0.1.0/rtube/services/__init__.py +1 -0
- rtube-0.1.0/rtube/services/encoder.py +232 -0
- rtube-0.1.0/rtube/services/ldap_auth.py +215 -0
- rtube-0.1.0/rtube/services/oidc_auth.py +257 -0
- rtube-0.1.0/rtube/services/saml_auth.py +285 -0
- rtube-0.1.0/rtube/static/js/plugin-test.js +21 -0
- rtube-0.1.0/rtube/static/js/video-player.js +44 -0
- rtube-0.1.0/rtube/static/package-lock.json +543 -0
- rtube-0.1.0/rtube/static/package.json +21 -0
- rtube-0.1.0/rtube/templates/400.html +76 -0
- rtube-0.1.0/rtube/templates/401.html +94 -0
- rtube-0.1.0/rtube/templates/403.html +125 -0
- rtube-0.1.0/rtube/templates/404.html +97 -0
- rtube-0.1.0/rtube/templates/405.html +76 -0
- rtube-0.1.0/rtube/templates/500.html +114 -0
- rtube-0.1.0/rtube/templates/501.html +76 -0
- rtube-0.1.0/rtube/templates/503.html +112 -0
- rtube-0.1.0/rtube/templates/admin/change_password.html +171 -0
- rtube-0.1.0/rtube/templates/admin/import_videos.html +289 -0
- rtube-0.1.0/rtube/templates/admin/users.html +297 -0
- rtube-0.1.0/rtube/templates/auth/login.html +219 -0
- rtube-0.1.0/rtube/templates/auth/profile.html +353 -0
- rtube-0.1.0/rtube/templates/auth/register.html +279 -0
- rtube-0.1.0/rtube/templates/encoding/jobs.html +228 -0
- rtube-0.1.0/rtube/templates/encoding/status.html +259 -0
- rtube-0.1.0/rtube/templates/encoding/upload.html +388 -0
- rtube-0.1.0/rtube/templates/index.html +711 -0
- rtube-0.1.0/rtube/templates/playlists/create.html +160 -0
- rtube-0.1.0/rtube/templates/playlists/edit.html +160 -0
- rtube-0.1.0/rtube/templates/playlists/index.html +205 -0
- rtube-0.1.0/rtube/templates/playlists/modal_content.html +38 -0
- rtube-0.1.0/rtube/templates/playlists/view.html +251 -0
- rtube-0.1.0/rtube/templates/videos/edit.html +287 -0
- rtube-0.1.0/rtube/templates/videos.html +561 -0
- rtube-0.1.0/rtube.egg-info/PKG-INFO +25 -0
- rtube-0.1.0/rtube.egg-info/SOURCES.txt +62 -0
- rtube-0.1.0/rtube.egg-info/dependency_links.txt +1 -0
- rtube-0.1.0/rtube.egg-info/requires.txt +13 -0
- rtube-0.1.0/rtube.egg-info/top_level.txt +4 -0
- rtube-0.1.0/setup.cfg +4 -0
- rtube-0.1.0/tools/mp4_to_hls.py +42 -0
- rtube-0.1.0/tools/populate_videos.py +616 -0
rtube-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Richard Dally
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
rtube-0.1.0/MANIFEST.in
ADDED
rtube-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rtube
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Streaming platform from scratch
|
|
5
|
+
Author: Richard Dally
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/RichardDally/RTube
|
|
8
|
+
Project-URL: Repository, https://github.com/RichardDally/RTube
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: Flask>=3.0.0
|
|
13
|
+
Requires-Dist: flask-sqlalchemy>=3.0.0
|
|
14
|
+
Requires-Dist: flask-login>=0.6.0
|
|
15
|
+
Requires-Dist: flask-migrate>=4.0.0
|
|
16
|
+
Requires-Dist: flask-session>=0.8.0
|
|
17
|
+
Requires-Dist: argon2-cffi>=23.1.0
|
|
18
|
+
Requires-Dist: psycopg2-binary
|
|
19
|
+
Requires-Dist: python-ffmpeg-video-streaming==0.1.14
|
|
20
|
+
Requires-Dist: ldap3>=2.9.0
|
|
21
|
+
Requires-Dist: authlib>=1.3.0
|
|
22
|
+
Requires-Dist: requests>=2.31.0
|
|
23
|
+
Requires-Dist: python3-saml>=1.16.0
|
|
24
|
+
Requires-Dist: python-dotenv>=1.2.1
|
|
25
|
+
Dynamic: license-file
|
rtube-0.1.0/README.md
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# RTube
|
|
2
|
+
Streaming platform Proof Of Concept.
|
|
3
|
+
|
|
4
|
+
## Prerequisites
|
|
5
|
+
|
|
6
|
+
* [Python 3.11+](https://www.python.org/downloads/)
|
|
7
|
+
* [Node.js](https://nodejs.org/en/download)
|
|
8
|
+
* [FFmpeg](https://ffmpeg.org/download.html) (must be in your `PATH`)
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
### 1. Install Python dependencies
|
|
13
|
+
|
|
14
|
+
Using [uv](https://docs.astral.sh/uv/) (recommended):
|
|
15
|
+
```bash
|
|
16
|
+
uv sync
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 2. Install Node.js dependencies
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd rtube/static
|
|
23
|
+
npm install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### Generate HLS playlist
|
|
29
|
+
|
|
30
|
+
Convert your MP4 video to HLS format:
|
|
31
|
+
```bash
|
|
32
|
+
python mp4_to_hls.py
|
|
33
|
+
```
|
|
34
|
+
This can take some time depending on your CPU.
|
|
35
|
+
|
|
36
|
+
### Run the server
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
flask --app rtube run
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Then open http://127.0.0.1:5000 in your browser.
|
|
43
|
+
|
|
44
|
+
## Features
|
|
45
|
+
|
|
46
|
+
### Video Player
|
|
47
|
+
|
|
48
|
+
- HLS streaming with adaptive quality selection
|
|
49
|
+
- Keyboard shortcuts (hotkeys)
|
|
50
|
+
- Video markers support
|
|
51
|
+
- Timestamp sharing via URL parameter (`?t=120` for 2 minutes)
|
|
52
|
+
|
|
53
|
+
### Video Management
|
|
54
|
+
|
|
55
|
+
- Upload and encode videos to HLS format
|
|
56
|
+
- Video visibility (public/private)
|
|
57
|
+
- Video deletion by owner or admin
|
|
58
|
+
- Thumbnail generation
|
|
59
|
+
- View count tracking
|
|
60
|
+
|
|
61
|
+
### Comments
|
|
62
|
+
|
|
63
|
+
- Post, edit, and delete comments on videos
|
|
64
|
+
- Automatic URL detection and linking (urlize)
|
|
65
|
+
- Character limit (5000 characters)
|
|
66
|
+
|
|
67
|
+
### Playlists
|
|
68
|
+
|
|
69
|
+
- Create and manage custom playlists
|
|
70
|
+
- Add/remove videos from playlists
|
|
71
|
+
- Reorder videos within playlists
|
|
72
|
+
- Public playlist viewing
|
|
73
|
+
|
|
74
|
+
### Favorites
|
|
75
|
+
|
|
76
|
+
- Mark videos as favorites
|
|
77
|
+
- Quick access to favorite videos from profile
|
|
78
|
+
|
|
79
|
+
### Search
|
|
80
|
+
|
|
81
|
+
- Search videos by title, description, or author
|
|
82
|
+
- Results grouped by match type
|
|
83
|
+
|
|
84
|
+
### Share Button
|
|
85
|
+
|
|
86
|
+
Each video page includes a share button that copies the current URL to the clipboard. The button provides visual feedback when the URL is copied.
|
|
87
|
+
|
|
88
|
+
## Environment Variables
|
|
89
|
+
|
|
90
|
+
| Variable | Description | Default |
|
|
91
|
+
|----------|-------------|---------|
|
|
92
|
+
| `RTUBE_DATABASE_URL` | Database connection URL (PostgreSQL recommended for production) | `sqlite:///rtube.db` |
|
|
93
|
+
| `RTUBE_AUTH_DATABASE_URL` | Authentication database URL (separate for security) | `sqlite:///rtube_auth.db` |
|
|
94
|
+
| `RTUBE_SECRET_KEY` | Secret key for session security (generate a strong random key for production) | Auto-generated |
|
|
95
|
+
| `RTUBE_HTTPS` | Enable secure session cookies (`true`, `1`, or `yes` when using HTTPS) | `false` |
|
|
96
|
+
| `RTUBE_KEEP_ORIGINAL_VIDEO` | Keep original MP4 file after encoding (`true`, `1`, or `yes` to enable) | `false` |
|
|
97
|
+
| `RTUBE_INSTANCE_PATH` | Custom path for instance folder (sessions, secret key). Must be an absolute path. | `instance/` |
|
|
98
|
+
| `RTUBE_LDAP_ENABLED` | Enable LDAP authentication (`true`, `1`, or `yes`) | `false` |
|
|
99
|
+
| `RTUBE_LDAP_SERVER` | LDAP server URL | `ldap://localhost:389` |
|
|
100
|
+
| `RTUBE_LDAP_USE_SSL` | Use SSL/TLS for LDAP connection | `false` |
|
|
101
|
+
| `RTUBE_LDAP_BIND_DN` | DN for LDAP bind (service account) | - |
|
|
102
|
+
| `RTUBE_LDAP_BIND_PASSWORD` | Password for LDAP bind | - |
|
|
103
|
+
| `RTUBE_LDAP_USER_BASE` | Base DN for user search | - |
|
|
104
|
+
| `RTUBE_LDAP_USER_FILTER` | LDAP filter for user search | `(uid={username})` |
|
|
105
|
+
| `RTUBE_LDAP_USERNAME_ATTRIBUTE` | LDAP attribute containing username | `uid` |
|
|
106
|
+
|
|
107
|
+
## Authentication
|
|
108
|
+
|
|
109
|
+
RTube includes a built-in authentication system with four user roles:
|
|
110
|
+
|
|
111
|
+
- **Anonymous**: Can view public videos only (not logged in)
|
|
112
|
+
- **Viewer**: Can view videos, create playlists, and add favorites, but cannot upload
|
|
113
|
+
- **Uploader**: Can view and upload videos, encode videos, manage own content
|
|
114
|
+
- **Admin**: Full access including user management, role changes, and moderation
|
|
115
|
+
|
|
116
|
+
### User Profiles
|
|
117
|
+
|
|
118
|
+
Each user has a profile page accessible at `/profile` (own profile) or `/profile/<username>` (any authenticated user). Profiles display:
|
|
119
|
+
- Uploaded videos with thumbnails and view counts
|
|
120
|
+
- Posted comments with links to the videos
|
|
121
|
+
|
|
122
|
+
### Admin Features
|
|
123
|
+
|
|
124
|
+
Administrators have access to the **Admin** dropdown menu which provides:
|
|
125
|
+
|
|
126
|
+
#### User Management (`/admin/users`)
|
|
127
|
+
- List of all registered users with their roles
|
|
128
|
+
- Online/offline status based on recent activity
|
|
129
|
+
- Video, comment, playlist, and favorite counts per user
|
|
130
|
+
- Role management: change user roles (Viewer, Uploader, Admin)
|
|
131
|
+
- Direct links to user profiles for moderation
|
|
132
|
+
- Password change for admin account
|
|
133
|
+
|
|
134
|
+
#### Import Videos (`/admin/import-videos`)
|
|
135
|
+
- Scan for orphan encoded videos (HLS files not in database)
|
|
136
|
+
- Display available quality variants for each video
|
|
137
|
+
- Bulk import with automatic thumbnail generation
|
|
138
|
+
- Videos are imported as private by default
|
|
139
|
+
|
|
140
|
+
#### Video Editing (`/watch/edit`)
|
|
141
|
+
- Admins can edit any video (not just their own)
|
|
142
|
+
- Change video owner to any Uploader or Admin user
|
|
143
|
+
|
|
144
|
+
### Session Persistence
|
|
145
|
+
|
|
146
|
+
User sessions persist across server restarts. Sessions are stored server-side using Flask-Session with filesystem storage. The secret key is automatically generated and saved to `instance/.secret_key` on first run.
|
|
147
|
+
|
|
148
|
+
### Storage
|
|
149
|
+
|
|
150
|
+
All media files are stored in the `instance/` folder:
|
|
151
|
+
- `instance/videos/` - HLS video files (.m3u8 and .ts segments)
|
|
152
|
+
- `instance/thumbnails/` - Video thumbnail images
|
|
153
|
+
- `instance/sessions/` - User session data
|
|
154
|
+
- `instance/.secret_key` - Persistent secret key
|
|
155
|
+
|
|
156
|
+
Use `RTUBE_INSTANCE_PATH` to customize the storage location.
|
|
157
|
+
|
|
158
|
+
### Default Admin Account
|
|
159
|
+
|
|
160
|
+
On first startup, a default admin account is created:
|
|
161
|
+
- **Username**: `admin`
|
|
162
|
+
- **Password**: `admin`
|
|
163
|
+
|
|
164
|
+
**Important**: Change this password immediately in production!
|
|
165
|
+
|
|
166
|
+
### LDAP Authentication
|
|
167
|
+
|
|
168
|
+
RTube supports LDAP authentication as an alternative to local accounts. When LDAP is enabled:
|
|
169
|
+
|
|
170
|
+
- All users authenticate via LDAP (except the local `admin` account)
|
|
171
|
+
- User accounts are auto-created on first LDAP login
|
|
172
|
+
- Local registration is disabled
|
|
173
|
+
- The local `admin` account can still login with its password (fallback for emergencies)
|
|
174
|
+
|
|
175
|
+
#### Configuration Example
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
export RTUBE_LDAP_ENABLED=true
|
|
179
|
+
export RTUBE_LDAP_SERVER=ldap://ldap.example.com:389
|
|
180
|
+
export RTUBE_LDAP_BIND_DN="cn=readonly,dc=example,dc=com"
|
|
181
|
+
export RTUBE_LDAP_BIND_PASSWORD="secret"
|
|
182
|
+
export RTUBE_LDAP_USER_BASE="ou=users,dc=example,dc=com"
|
|
183
|
+
export RTUBE_LDAP_USER_FILTER="(uid={username})"
|
|
184
|
+
export RTUBE_LDAP_USERNAME_ATTRIBUTE="uid"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### For Active Directory
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
export RTUBE_LDAP_SERVER=ldap://ad.example.com:389
|
|
191
|
+
export RTUBE_LDAP_BIND_DN="CN=Service Account,OU=Service Accounts,DC=example,DC=com"
|
|
192
|
+
export RTUBE_LDAP_USER_BASE="OU=Users,DC=example,DC=com"
|
|
193
|
+
export RTUBE_LDAP_USER_FILTER="(sAMAccountName={username})"
|
|
194
|
+
export RTUBE_LDAP_USERNAME_ATTRIBUTE="sAMAccountName"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### How it Works
|
|
198
|
+
|
|
199
|
+
1. User enters LDAP credentials on the login page
|
|
200
|
+
2. RTube searches for the user in LDAP using the configured filter
|
|
201
|
+
3. If found, RTube attempts to bind with the user's DN and password
|
|
202
|
+
4. On successful authentication, a local user record is created (if first login)
|
|
203
|
+
5. The user is logged in with the `uploader` role
|
|
204
|
+
|
|
205
|
+
### SSO Authentication (OIDC / SAML)
|
|
206
|
+
|
|
207
|
+
RTube supports Single Sign-On via OpenID Connect (OIDC) and SAML 2.0 protocols. These can be enabled alongside local and LDAP authentication.
|
|
208
|
+
|
|
209
|
+
#### OIDC Configuration (Keycloak, Azure AD, Okta, etc.)
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
export RTUBE_OIDC_ENABLED=true
|
|
213
|
+
export RTUBE_OIDC_CLIENT_ID="your-client-id"
|
|
214
|
+
export RTUBE_OIDC_CLIENT_SECRET="your-client-secret"
|
|
215
|
+
export RTUBE_OIDC_DISCOVERY_URL="https://idp.example.com/.well-known/openid-configuration"
|
|
216
|
+
export RTUBE_OIDC_SCOPES="openid profile email"
|
|
217
|
+
export RTUBE_OIDC_USERNAME_CLAIM="preferred_username"
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
| Variable | Description | Default |
|
|
221
|
+
|----------|-------------|---------|
|
|
222
|
+
| `RTUBE_OIDC_ENABLED` | Enable OIDC authentication | `false` |
|
|
223
|
+
| `RTUBE_OIDC_CLIENT_ID` | OAuth2 client ID | - |
|
|
224
|
+
| `RTUBE_OIDC_CLIENT_SECRET` | OAuth2 client secret | - |
|
|
225
|
+
| `RTUBE_OIDC_DISCOVERY_URL` | OIDC discovery endpoint URL | - |
|
|
226
|
+
| `RTUBE_OIDC_SCOPES` | Space-separated OAuth2 scopes | `openid profile email` |
|
|
227
|
+
| `RTUBE_OIDC_USERNAME_CLAIM` | Claim to use for username | `preferred_username` |
|
|
228
|
+
|
|
229
|
+
**Callback URL**: Configure your IdP with the callback URL: `https://your-rtube-domain/auth/oidc/callback`
|
|
230
|
+
|
|
231
|
+
#### SAML 2.0 Configuration (ADFS, Okta, Shibboleth, etc.)
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
export RTUBE_SAML_ENABLED=true
|
|
235
|
+
export RTUBE_SAML_IDP_ENTITY_ID="https://idp.example.com"
|
|
236
|
+
export RTUBE_SAML_IDP_SSO_URL="https://idp.example.com/sso"
|
|
237
|
+
export RTUBE_SAML_IDP_CERT_FILE="/path/to/idp-cert.pem"
|
|
238
|
+
export RTUBE_SAML_SP_ENTITY_ID="https://rtube.example.com"
|
|
239
|
+
export RTUBE_SAML_USERNAME_ATTRIBUTE="uid"
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
| Variable | Description | Default |
|
|
243
|
+
|----------|-------------|---------|
|
|
244
|
+
| `RTUBE_SAML_ENABLED` | Enable SAML authentication | `false` |
|
|
245
|
+
| `RTUBE_SAML_IDP_ENTITY_ID` | IdP Entity ID | - |
|
|
246
|
+
| `RTUBE_SAML_IDP_SSO_URL` | IdP Single Sign-On URL | - |
|
|
247
|
+
| `RTUBE_SAML_IDP_CERT_FILE` | Path to IdP certificate file | - |
|
|
248
|
+
| `RTUBE_SAML_IDP_CERT` | IdP certificate (alternative to file) | - |
|
|
249
|
+
| `RTUBE_SAML_SP_ENTITY_ID` | Service Provider Entity ID | Auto-generated |
|
|
250
|
+
| `RTUBE_SAML_USERNAME_ATTRIBUTE` | SAML attribute for username | `uid` |
|
|
251
|
+
| `RTUBE_SAML_EMAIL_ATTRIBUTE` | SAML attribute for email | `email` |
|
|
252
|
+
| `RTUBE_SAML_NAME_ATTRIBUTE` | SAML attribute for display name | `displayName` |
|
|
253
|
+
|
|
254
|
+
**Service Provider Metadata**: Available at `https://your-rtube-domain/auth/saml/metadata`
|
|
255
|
+
|
|
256
|
+
**Assertion Consumer Service URL**: `https://your-rtube-domain/auth/saml/acs`
|
|
257
|
+
|
|
258
|
+
#### How SSO Works
|
|
259
|
+
|
|
260
|
+
1. User clicks "Sign in with SSO" on the login page
|
|
261
|
+
2. User is redirected to the Identity Provider (IdP)
|
|
262
|
+
3. After successful authentication, IdP redirects back to RTube
|
|
263
|
+
4. RTube creates a local user account on first login (with `uploader` role)
|
|
264
|
+
5. User is logged in
|
|
265
|
+
|
|
266
|
+
SSO users cannot change their password in RTube - authentication is managed by the IdP.
|
|
267
|
+
|
|
268
|
+
### Password Requirements
|
|
269
|
+
|
|
270
|
+
- Minimum 12 characters
|
|
271
|
+
- At least one uppercase letter (A-Z)
|
|
272
|
+
- At least one lowercase letter (a-z)
|
|
273
|
+
- At least one digit (0-9)
|
|
274
|
+
- At least one special character
|
|
275
|
+
- No common patterns or sequences
|
|
276
|
+
|
|
277
|
+
## Database Migrations
|
|
278
|
+
|
|
279
|
+
RTube uses [Flask-Migrate](https://flask-migrate.readthedocs.io/) (Alembic) to manage database schema changes.
|
|
280
|
+
|
|
281
|
+
### For New Installations
|
|
282
|
+
|
|
283
|
+
If you're setting up RTube for the first time, the database will be created automatically when you start the application. Then stamp the database to mark it as up-to-date:
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
flask --app rtube.app:create_app db stamp head
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Applying Migrations
|
|
290
|
+
|
|
291
|
+
After pulling new changes that include database migrations:
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
flask --app rtube.app:create_app db upgrade
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Auth Database Migrations
|
|
298
|
+
|
|
299
|
+
The `users` table is stored in a separate auth database (`rtube_auth.db` or PostgreSQL). Flask-Migrate only manages the main database, so auth schema changes must be applied manually.
|
|
300
|
+
|
|
301
|
+
**For LDAP support (adding `auth_type` column):**
|
|
302
|
+
|
|
303
|
+
SQLite:
|
|
304
|
+
```bash
|
|
305
|
+
sqlite3 instance/rtube_auth.db "ALTER TABLE users ADD COLUMN auth_type VARCHAR(10) NOT NULL DEFAULT 'local';"
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
PostgreSQL:
|
|
309
|
+
```sql
|
|
310
|
+
ALTER TABLE users ADD COLUMN auth_type VARCHAR(10) NOT NULL DEFAULT 'local';
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**For SSO support (adding `sso_subject` column):**
|
|
314
|
+
|
|
315
|
+
SQLite:
|
|
316
|
+
```bash
|
|
317
|
+
sqlite3 instance/rtube_auth.db "ALTER TABLE users ADD COLUMN sso_subject VARCHAR(255);"
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
PostgreSQL:
|
|
321
|
+
```sql
|
|
322
|
+
ALTER TABLE users ADD COLUMN sso_subject VARCHAR(255);
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**For role column (if upgrading from older version):**
|
|
326
|
+
|
|
327
|
+
The `role` column should already exist with default value `uploader`. Valid roles are: `viewer`, `uploader`, `admin`.
|
|
328
|
+
|
|
329
|
+
### Creating New Migrations
|
|
330
|
+
|
|
331
|
+
When you modify the data models (`models.py` or `models_auth.py`):
|
|
332
|
+
|
|
333
|
+
1. **Auto-generate a migration** based on model changes:
|
|
334
|
+
```bash
|
|
335
|
+
flask --app rtube.app:create_app db migrate -m "Description of changes"
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
2. **Review the generated migration** in `migrations/versions/` before applying it.
|
|
339
|
+
|
|
340
|
+
3. **Apply the migration**:
|
|
341
|
+
```bash
|
|
342
|
+
flask --app rtube.app:create_app db upgrade
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Common Commands
|
|
346
|
+
|
|
347
|
+
| Command | Description |
|
|
348
|
+
|---------|-------------|
|
|
349
|
+
| `flask db upgrade` | Apply all pending migrations |
|
|
350
|
+
| `flask db downgrade` | Revert the last migration |
|
|
351
|
+
| `flask db current` | Show current migration revision |
|
|
352
|
+
| `flask db history` | Show migration history |
|
|
353
|
+
| `flask db stamp head` | Mark database as up-to-date without running migrations |
|
|
354
|
+
|
|
355
|
+
**Note**: Always use `--app rtube.app:create_app` with Flask commands, or set the `FLASK_APP` environment variable:
|
|
356
|
+
```bash
|
|
357
|
+
export FLASK_APP=rtube.app:create_app # Linux/macOS
|
|
358
|
+
set FLASK_APP=rtube.app:create_app # Windows
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Git LFS side note
|
|
362
|
+
* Download and install [Git Large File Storage](https://git-lfs.github.com/)
|
|
363
|
+
* Track mp4 files `$ git lfs track "*.mp4"`
|
|
364
|
+
* `git add/commit/push` will upload on GitHub LFS.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from logging.config import fileConfig
|
|
3
|
+
|
|
4
|
+
from flask import current_app
|
|
5
|
+
|
|
6
|
+
from alembic import context
|
|
7
|
+
|
|
8
|
+
# this is the Alembic Config object, which provides
|
|
9
|
+
# access to the values within the .ini file in use.
|
|
10
|
+
config = context.config
|
|
11
|
+
|
|
12
|
+
# Interpret the config file for Python logging.
|
|
13
|
+
# This line sets up loggers basically.
|
|
14
|
+
fileConfig(config.config_file_name)
|
|
15
|
+
logger = logging.getLogger('alembic.env')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_engine():
|
|
19
|
+
try:
|
|
20
|
+
# this works with Flask-SQLAlchemy<3 and Alchemical
|
|
21
|
+
return current_app.extensions['migrate'].db.get_engine()
|
|
22
|
+
except (TypeError, AttributeError):
|
|
23
|
+
# this works with Flask-SQLAlchemy>=3
|
|
24
|
+
return current_app.extensions['migrate'].db.engine
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_engine_url():
|
|
28
|
+
try:
|
|
29
|
+
return get_engine().url.render_as_string(hide_password=False).replace(
|
|
30
|
+
'%', '%%')
|
|
31
|
+
except AttributeError:
|
|
32
|
+
return str(get_engine().url).replace('%', '%%')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# add your model's MetaData object here
|
|
36
|
+
# for 'autogenerate' support
|
|
37
|
+
# from myapp import mymodel
|
|
38
|
+
# target_metadata = mymodel.Base.metadata
|
|
39
|
+
config.set_main_option('sqlalchemy.url', get_engine_url())
|
|
40
|
+
target_db = current_app.extensions['migrate'].db
|
|
41
|
+
|
|
42
|
+
# other values from the config, defined by the needs of env.py,
|
|
43
|
+
# can be acquired:
|
|
44
|
+
# my_important_option = config.get_main_option("my_important_option")
|
|
45
|
+
# ... etc.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_metadata():
|
|
49
|
+
if hasattr(target_db, 'metadatas'):
|
|
50
|
+
return target_db.metadatas[None]
|
|
51
|
+
return target_db.metadata
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run_migrations_offline():
|
|
55
|
+
"""Run migrations in 'offline' mode.
|
|
56
|
+
|
|
57
|
+
This configures the context with just a URL
|
|
58
|
+
and not an Engine, though an Engine is acceptable
|
|
59
|
+
here as well. By skipping the Engine creation
|
|
60
|
+
we don't even need a DBAPI to be available.
|
|
61
|
+
|
|
62
|
+
Calls to context.execute() here emit the given string to the
|
|
63
|
+
script output.
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
67
|
+
context.configure(
|
|
68
|
+
url=url, target_metadata=get_metadata(), literal_binds=True
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
with context.begin_transaction():
|
|
72
|
+
context.run_migrations()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def run_migrations_online():
|
|
76
|
+
"""Run migrations in 'online' mode.
|
|
77
|
+
|
|
78
|
+
In this scenario we need to create an Engine
|
|
79
|
+
and associate a connection with the context.
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
# this callback is used to prevent an auto-migration from being generated
|
|
84
|
+
# when there are no changes to the schema
|
|
85
|
+
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
|
86
|
+
def process_revision_directives(context, revision, directives):
|
|
87
|
+
if getattr(config.cmd_opts, 'autogenerate', False):
|
|
88
|
+
script = directives[0]
|
|
89
|
+
if script.upgrade_ops.is_empty():
|
|
90
|
+
directives[:] = []
|
|
91
|
+
logger.info('No changes in schema detected.')
|
|
92
|
+
|
|
93
|
+
conf_args = current_app.extensions['migrate'].configure_args
|
|
94
|
+
if conf_args.get("process_revision_directives") is None:
|
|
95
|
+
conf_args["process_revision_directives"] = process_revision_directives
|
|
96
|
+
|
|
97
|
+
connectable = get_engine()
|
|
98
|
+
|
|
99
|
+
with connectable.connect() as connection:
|
|
100
|
+
context.configure(
|
|
101
|
+
connection=connection,
|
|
102
|
+
target_metadata=get_metadata(),
|
|
103
|
+
**conf_args
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
with context.begin_transaction():
|
|
107
|
+
context.run_migrations()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if context.is_offline_mode():
|
|
111
|
+
run_migrations_offline()
|
|
112
|
+
else:
|
|
113
|
+
run_migrations_online()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Add auth_type column to users table
|
|
2
|
+
|
|
3
|
+
Revision ID: add_auth_type_to_users
|
|
4
|
+
Revises: add_playlists_tables
|
|
5
|
+
Create Date: 2026-01-01
|
|
6
|
+
|
|
7
|
+
Note: The users table is in the 'auth' bind, not the main database.
|
|
8
|
+
This migration must be applied manually to the auth database.
|
|
9
|
+
"""
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision = 'add_auth_type_to_users'
|
|
16
|
+
down_revision = 'add_playlists_tables'
|
|
17
|
+
branch_labels = None
|
|
18
|
+
depends_on = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade():
|
|
22
|
+
# Note: The users table is in the 'auth' bind (separate database).
|
|
23
|
+
# Flask-Migrate doesn't handle binds automatically, so this migration
|
|
24
|
+
# needs to connect to the auth database.
|
|
25
|
+
#
|
|
26
|
+
# For SQLite auth database, run manually:
|
|
27
|
+
# sqlite3 instance/rtube_auth.db
|
|
28
|
+
# ALTER TABLE users ADD COLUMN auth_type VARCHAR(10) NOT NULL DEFAULT 'local';
|
|
29
|
+
#
|
|
30
|
+
# For PostgreSQL auth database:
|
|
31
|
+
# psql -d rtube_auth
|
|
32
|
+
# ALTER TABLE users ADD COLUMN auth_type VARCHAR(10) NOT NULL DEFAULT 'local';
|
|
33
|
+
#
|
|
34
|
+
# The password_hash column is already nullable in SQLite (SQLite ignores NOT NULL
|
|
35
|
+
# constraints on ALTER TABLE), but for PostgreSQL you may need:
|
|
36
|
+
# ALTER TABLE users ALTER COLUMN password_hash DROP NOT NULL;
|
|
37
|
+
|
|
38
|
+
# Try to apply to the auth bind if available
|
|
39
|
+
bind = op.get_bind()
|
|
40
|
+
inspector = sa.inspect(bind)
|
|
41
|
+
|
|
42
|
+
# Check if users table exists in this database
|
|
43
|
+
if 'users' in inspector.get_table_names():
|
|
44
|
+
# Check if auth_type column already exists
|
|
45
|
+
columns = [col['name'] for col in inspector.get_columns('users')]
|
|
46
|
+
if 'auth_type' not in columns:
|
|
47
|
+
op.add_column(
|
|
48
|
+
'users',
|
|
49
|
+
sa.Column('auth_type', sa.String(length=10), nullable=False, server_default='local'),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def downgrade():
|
|
54
|
+
bind = op.get_bind()
|
|
55
|
+
inspector = sa.inspect(bind)
|
|
56
|
+
|
|
57
|
+
if 'users' in inspector.get_table_names():
|
|
58
|
+
columns = [col['name'] for col in inspector.get_columns('users')]
|
|
59
|
+
if 'auth_type' in columns:
|
|
60
|
+
op.drop_column('users', 'auth_type')
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Add favorites table
|
|
2
|
+
|
|
3
|
+
Revision ID: add_favorites_table
|
|
4
|
+
Revises: ce7c7ff2a0aa
|
|
5
|
+
Create Date: 2025-12-30
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from alembic import op
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# revision identifiers, used by Alembic.
|
|
13
|
+
revision = 'add_favorites_table'
|
|
14
|
+
down_revision = 'ce7c7ff2a0aa'
|
|
15
|
+
branch_labels = None
|
|
16
|
+
depends_on = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def upgrade():
|
|
20
|
+
# Favorites table
|
|
21
|
+
op.create_table('favorites',
|
|
22
|
+
sa.Column('id', sa.Integer(), nullable=False),
|
|
23
|
+
sa.Column('username', sa.String(length=80), nullable=False),
|
|
24
|
+
sa.Column('video_id', sa.Integer(), nullable=False),
|
|
25
|
+
sa.Column('created_at', sa.DateTime(), nullable=False),
|
|
26
|
+
sa.ForeignKeyConstraint(['video_id'], ['videos.id'], ),
|
|
27
|
+
sa.PrimaryKeyConstraint('id'),
|
|
28
|
+
sa.UniqueConstraint('username', 'video_id', name='unique_user_video_favorite')
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def downgrade():
|
|
33
|
+
op.drop_table('favorites')
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Add playlists tables
|
|
2
|
+
|
|
3
|
+
Revision ID: add_playlists_tables
|
|
4
|
+
Revises: add_favorites_table
|
|
5
|
+
Create Date: 2025-12-31
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from alembic import op
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# revision identifiers, used by Alembic.
|
|
13
|
+
revision = 'add_playlists_tables'
|
|
14
|
+
down_revision = 'add_favorites_table'
|
|
15
|
+
branch_labels = None
|
|
16
|
+
depends_on = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def upgrade():
|
|
20
|
+
# Playlists table
|
|
21
|
+
op.create_table('playlists',
|
|
22
|
+
sa.Column('id', sa.Integer(), nullable=False),
|
|
23
|
+
sa.Column('name', sa.String(length=255), nullable=False),
|
|
24
|
+
sa.Column('description', sa.String(length=5000), nullable=True),
|
|
25
|
+
sa.Column('owner_username', sa.String(length=80), nullable=False),
|
|
26
|
+
sa.Column('created_at', sa.DateTime(), nullable=False),
|
|
27
|
+
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
|
28
|
+
sa.PrimaryKeyConstraint('id')
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Playlist videos junction table
|
|
32
|
+
op.create_table('playlist_videos',
|
|
33
|
+
sa.Column('id', sa.Integer(), nullable=False),
|
|
34
|
+
sa.Column('playlist_id', sa.Integer(), nullable=False),
|
|
35
|
+
sa.Column('video_id', sa.Integer(), nullable=False),
|
|
36
|
+
sa.Column('position', sa.Integer(), nullable=False),
|
|
37
|
+
sa.Column('added_at', sa.DateTime(), nullable=False),
|
|
38
|
+
sa.ForeignKeyConstraint(['playlist_id'], ['playlists.id'], ),
|
|
39
|
+
sa.ForeignKeyConstraint(['video_id'], ['videos.id'], ),
|
|
40
|
+
sa.PrimaryKeyConstraint('id'),
|
|
41
|
+
sa.UniqueConstraint('playlist_id', 'video_id', name='unique_playlist_video')
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def downgrade():
|
|
46
|
+
op.drop_table('playlist_videos')
|
|
47
|
+
op.drop_table('playlists')
|