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.
Files changed (64) hide show
  1. rtube-0.1.0/LICENSE +21 -0
  2. rtube-0.1.0/MANIFEST.in +6 -0
  3. rtube-0.1.0/PKG-INFO +25 -0
  4. rtube-0.1.0/README.md +364 -0
  5. rtube-0.1.0/migrations/env.py +113 -0
  6. rtube-0.1.0/migrations/versions/add_auth_type_to_users.py +60 -0
  7. rtube-0.1.0/migrations/versions/add_favorites_table.py +33 -0
  8. rtube-0.1.0/migrations/versions/add_playlists_tables.py +47 -0
  9. rtube-0.1.0/migrations/versions/add_sso_subject_to_users.py +56 -0
  10. rtube-0.1.0/migrations/versions/ce7c7ff2a0aa_initial_schema.py +94 -0
  11. rtube-0.1.0/pyproject.toml +53 -0
  12. rtube-0.1.0/rtube/__init__.py +4 -0
  13. rtube-0.1.0/rtube/__version__.py +1 -0
  14. rtube-0.1.0/rtube/app.py +338 -0
  15. rtube-0.1.0/rtube/models.py +133 -0
  16. rtube-0.1.0/rtube/models_auth.py +177 -0
  17. rtube-0.1.0/rtube/routes/__init__.py +4 -0
  18. rtube-0.1.0/rtube/routes/admin.py +266 -0
  19. rtube-0.1.0/rtube/routes/auth.py +435 -0
  20. rtube-0.1.0/rtube/routes/encoding.py +130 -0
  21. rtube-0.1.0/rtube/routes/playlists.py +215 -0
  22. rtube-0.1.0/rtube/routes/videos.py +551 -0
  23. rtube-0.1.0/rtube/services/__init__.py +1 -0
  24. rtube-0.1.0/rtube/services/encoder.py +232 -0
  25. rtube-0.1.0/rtube/services/ldap_auth.py +215 -0
  26. rtube-0.1.0/rtube/services/oidc_auth.py +257 -0
  27. rtube-0.1.0/rtube/services/saml_auth.py +285 -0
  28. rtube-0.1.0/rtube/static/js/plugin-test.js +21 -0
  29. rtube-0.1.0/rtube/static/js/video-player.js +44 -0
  30. rtube-0.1.0/rtube/static/package-lock.json +543 -0
  31. rtube-0.1.0/rtube/static/package.json +21 -0
  32. rtube-0.1.0/rtube/templates/400.html +76 -0
  33. rtube-0.1.0/rtube/templates/401.html +94 -0
  34. rtube-0.1.0/rtube/templates/403.html +125 -0
  35. rtube-0.1.0/rtube/templates/404.html +97 -0
  36. rtube-0.1.0/rtube/templates/405.html +76 -0
  37. rtube-0.1.0/rtube/templates/500.html +114 -0
  38. rtube-0.1.0/rtube/templates/501.html +76 -0
  39. rtube-0.1.0/rtube/templates/503.html +112 -0
  40. rtube-0.1.0/rtube/templates/admin/change_password.html +171 -0
  41. rtube-0.1.0/rtube/templates/admin/import_videos.html +289 -0
  42. rtube-0.1.0/rtube/templates/admin/users.html +297 -0
  43. rtube-0.1.0/rtube/templates/auth/login.html +219 -0
  44. rtube-0.1.0/rtube/templates/auth/profile.html +353 -0
  45. rtube-0.1.0/rtube/templates/auth/register.html +279 -0
  46. rtube-0.1.0/rtube/templates/encoding/jobs.html +228 -0
  47. rtube-0.1.0/rtube/templates/encoding/status.html +259 -0
  48. rtube-0.1.0/rtube/templates/encoding/upload.html +388 -0
  49. rtube-0.1.0/rtube/templates/index.html +711 -0
  50. rtube-0.1.0/rtube/templates/playlists/create.html +160 -0
  51. rtube-0.1.0/rtube/templates/playlists/edit.html +160 -0
  52. rtube-0.1.0/rtube/templates/playlists/index.html +205 -0
  53. rtube-0.1.0/rtube/templates/playlists/modal_content.html +38 -0
  54. rtube-0.1.0/rtube/templates/playlists/view.html +251 -0
  55. rtube-0.1.0/rtube/templates/videos/edit.html +287 -0
  56. rtube-0.1.0/rtube/templates/videos.html +561 -0
  57. rtube-0.1.0/rtube.egg-info/PKG-INFO +25 -0
  58. rtube-0.1.0/rtube.egg-info/SOURCES.txt +62 -0
  59. rtube-0.1.0/rtube.egg-info/dependency_links.txt +1 -0
  60. rtube-0.1.0/rtube.egg-info/requires.txt +13 -0
  61. rtube-0.1.0/rtube.egg-info/top_level.txt +4 -0
  62. rtube-0.1.0/setup.cfg +4 -0
  63. rtube-0.1.0/tools/mp4_to_hls.py +42 -0
  64. 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.
@@ -0,0 +1,6 @@
1
+ recursive-include rtube/templates *.html
2
+ include rtube/static/js/*.js
3
+ include rtube/static/package.json
4
+ include rtube/static/package-lock.json
5
+ prune rtube/static/node_modules
6
+ prune tests
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')