dragxd commited on
Commit
19f62c1
·
0 Parent(s):

Initial commit for beta-tgdrive

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +35 -0
  2. .gitignore +9 -0
  3. Dockerfile +18 -0
  4. LICENSE +21 -0
  5. README.md +179 -0
  6. SETUP_SUMMARY.txt +27 -0
  7. cache/bot.session +0 -0
  8. cache/main_bot.session +0 -0
  9. config.py +89 -0
  10. docker_start.sh +3 -0
  11. main.py +528 -0
  12. render.yaml +24 -0
  13. requirements.txt +13 -0
  14. runtime.txt +1 -0
  15. sample.env +20 -0
  16. start_main.py +3 -0
  17. utils/bot_mode.py +266 -0
  18. utils/clients.py +208 -0
  19. utils/directoryHandler.py +409 -0
  20. utils/downloader.py +85 -0
  21. utils/extra.py +146 -0
  22. utils/logger.py +48 -0
  23. utils/mongo_indexer.py +121 -0
  24. utils/movie_requests.py +75 -0
  25. utils/streamer/__init__.py +82 -0
  26. utils/streamer/custom_dl.py +223 -0
  27. utils/streamer/file_properties.py +84 -0
  28. utils/uploader.py +99 -0
  29. website/VideoPlayer.html +286 -0
  30. website/home.html +219 -0
  31. website/static/assets/file-icon.svg +1 -0
  32. website/static/assets/folder-icon.svg +1 -0
  33. website/static/assets/folder-solid-icon.svg +1 -0
  34. website/static/assets/home-icon.svg +1 -0
  35. website/static/assets/info-icon-small.svg +1 -0
  36. website/static/assets/link-icon.svg +1 -0
  37. website/static/assets/load-icon.svg +1 -0
  38. website/static/assets/more-icon.svg +1 -0
  39. website/static/assets/pencil-icon.svg +1 -0
  40. website/static/assets/plus-icon.svg +1 -0
  41. website/static/assets/profile-icon.svg +1 -0
  42. website/static/assets/search-icon.svg +1 -0
  43. website/static/assets/share-icon.svg +1 -0
  44. website/static/assets/trash-icon.svg +1 -0
  45. website/static/assets/upload-icon.svg +1 -0
  46. website/static/home.css +1112 -0
  47. website/static/js/apiHandler.js +368 -0
  48. website/static/js/extra.js +119 -0
  49. website/static/js/fileClickHandler.js +222 -0
  50. website/static/js/main.js +216 -0
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ *.pyc
2
+ *.data
3
+ t.py
4
+ t2.py
5
+ cache/*
6
+ !cache/*.session
7
+ config copy.py
8
+ logs.txt
9
+ .env
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use the official Python base image
2
+ FROM python:3.11.7-slim AS base
3
+
4
+ # Set the working directory inside the container
5
+ WORKDIR /app
6
+
7
+ # Copy the requirements file to the working directory and install dependencies
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ # Copy the application code to the working directory
12
+ COPY . .
13
+
14
+ # Expose the port on which the application will run (Hugging Face Spaces uses 7860)
15
+ EXPOSE 7860
16
+
17
+ # Run the FastAPI application using uvicorn server
18
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 TechShreyash
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.
README.md ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: TGDRIVE
3
+ emoji: 🌍
4
+ colorFrom: red
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ # TGDrive - A Google Drive Clone with Telegram Storage
11
+
12
+ Welcome to TGDrive! This web application replicates Google Drive's functionalities using Telegram as its storage backend. Manage folders and files, perform actions like uploading, renaming, and deleting, utilize trash/bin support, enable permanent deletion, and share public links. The application offers admin login and automatically backs up the database to Telegram.
13
+
14
+ **Check out the [TGDrive Personal](https://github.com/TechShreyash/TGDrivePersonal) project for local desktop backup support.**
15
+
16
+ ## Features
17
+
18
+ - **File Management:** Upload, rename, and delete files with integrated trash/bin functionality and permanent deletion support.
19
+ - **Folder Management:** Easily create, rename, and delete folders.
20
+ - **Sharing:** Seamlessly share public links for files and folders.
21
+ - **Admin Support:** Secure admin login for efficient management.
22
+ - **Automatic Backups:** Automated database backups sent directly to Telegram.
23
+ - **Multiple Bots/Clients:** Support for multiple bots/clients for file operations and streaming from Telegram.
24
+ - **Large File Support:** Upload files up to 4GB using Telegram Premium accounts.
25
+ - **Auto Pinger:** Built-in feature to keep the website active by preventing idle timeouts.
26
+ - **URL Upload Support:** Upload files directly to TG Drive from any direct download link of a file.
27
+ - **Bot Mode:** Upload files directly to any folder in TG Drive by sending the file to the bot on Telegram ([Know More](#tg-drives-bot-mode))
28
+
29
+ ## Tech Stack
30
+
31
+ - **Backend:** Python, FastAPI
32
+ - **Frontend:** HTML, CSS, JavaScript
33
+ - **Database:** Local storage as a class object, saved to a file using the pickle module.
34
+ - **Storage:** Telegram
35
+
36
+ ### Environment Variables
37
+
38
+ #### Required Variables
39
+
40
+ | Variable Name | Type | Example | Description |
41
+ | ------------------------ | ------- | ------------------------- | -------------------------------------------------------------------- |
42
+ | `API_ID` | integer | 123456 | Your Telegram API ID |
43
+ | `API_HASH` | string | dagsjdhgjfsahgjfh | Your Telegram API Hash |
44
+ | `BOT_TOKENS` | string | 21413535:gkdshajfhjfakhjf | List of Telegram bot tokens for file operations, separated by commas |
45
+ | `STORAGE_CHANNEL` | integer | -100123456789 | Chat ID of the Telegram storage channel |
46
+ | `DATABASE_BACKUP_MSG_ID` | integer | 123 | Message ID of a file in the storage channel for database backups |
47
+
48
+ > Note: All bots mentioned in the `BOT_TOKENS` variable must be added as admins in your `STORAGE_CHANNEL`.
49
+
50
+ > Note: `DATABASE_BACKUP_MSG_ID` should be the message ID of a file (document) in the `STORAGE_CHANNEL`.
51
+
52
+ #### Optional Variables
53
+
54
+ | Variable Name | Type | Default | Description |
55
+ | ---------------------- | -------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
56
+ | `ADMIN_PASSWORD` | string | admin | Password for accessing the admin panel |
57
+ | `STRING_SESSIONS` | string | None | List of Premium Telegram Account Pyrogram String Sessions for file operations |
58
+ | `SLEEP_THRESHOLD` | integer (in seconds) | 60 | Delay in seconds before retrying after a Telegram API floodwait error |
59
+ | `DATABASE_BACKUP_TIME` | integer (in seconds) | 60 | Interval in seconds for database backups to the storage channel |
60
+ | `MAX_FILE_SIZE` | float (in GBs) | 1.98 (3.98 if `STRING_SESSIONS` are added) | Maximum file size (in GBs) allowed for uploading to Telegram |
61
+ | `WEBSITE_URL` | string | None | Website URL (with https/http) to auto-ping to keep the website active |
62
+ | `MAIN_BOT_TOKEN` | string | None | Your Main Bot Token to use [TG Drive's Bot Mode](#tg-drives-bot-mode) |
63
+ | `TELEGRAM_ADMIN_IDS` | string | None | List of Telegram User IDs of admins who can access the [bot mode](#tg-drives-bot-mode), separated by commas |
64
+
65
+ > Note: Premium Client (`STRING_SESSIONS`) will be used only to upload files when file size is greater than 2GB.
66
+
67
+ > Note: File streaming/downloads will be handled by bots (`BOT_TOKENS`).
68
+
69
+ > Note: Read more about TG Drive's Bot Mode [here](#tg-drives-bot-mode).
70
+
71
+ ## Deploying Your Own TG Drive Application
72
+
73
+ ### 1. Clone the Repository
74
+
75
+ First, clone the repository and navigate into the project directory:
76
+
77
+ ```bash
78
+ git clone https://github.com/TechShreyash/TGDrive
79
+ cd TGDrive
80
+ ```
81
+
82
+ ### 2. Set Up Your Environment Variables
83
+
84
+ Create a `.env` file in the root directory and add the necessary [environment variables](#environment-variables).
85
+
86
+ > **Note:** Some hosting services allow you to set environment variables directly through their interface, which may eliminate the need for a `.env` file.
87
+
88
+ ### 3. Running TG Drive
89
+
90
+ #### Deploying Locally
91
+
92
+ 1. Install the required Python packages:
93
+
94
+ ```bash
95
+ pip install -U -r requirements.txt
96
+ ```
97
+
98
+ 2. Start the TG Drive application using Uvicorn:
99
+
100
+ ```bash
101
+ uvicorn main:app --host 0.0.0.0 --port 8000
102
+ ```
103
+
104
+ #### Deploying Using Docker
105
+
106
+ 1. Build the Docker image:
107
+
108
+ ```bash
109
+ docker build -t tgdrive .
110
+ ```
111
+
112
+ 2. Run the Docker container:
113
+
114
+ ```bash
115
+ docker run -d -p 8000:8000 tgdrive
116
+ ```
117
+
118
+ Access the application at `http://127.0.0.1:8000` or `http://your_ip:8000`.
119
+
120
+ > **Note:** For more detailed information on deploying FastAPI applications, refer to online guides and resources.
121
+
122
+ ## Deploy Tutorials
123
+
124
+ **Deploy To Render.com For Free :** https://youtu.be/S5OIi5Ur3c0
125
+
126
+ <div align="center">
127
+
128
+ [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/TechShreyash/TGDrive)
129
+
130
+ </div>
131
+
132
+ > **Note:** After updating the TG Drive code, clear your browser's cache to ensure the latest JavaScript files are loaded and run correctly.
133
+
134
+ ## TG Drive's Bot Mode
135
+
136
+ TG Drive's Bot Mode is a new feature that allows you to upload files directly to your TG Drive website from a Telegram bot. Simply send or forward any file to the bot, and it will be uploaded to your TG Drive. You can also specify the folder where you want the files to be uploaded.
137
+
138
+ To use this feature, you need to set the configuration variables `MAIN_BOT_TOKEN` and `TELEGRAM_ADMIN_IDS`. More information about these variables can be found in the [optional variables section](#optional-variables).
139
+
140
+ Once these variables are set, users whose IDs are listed in `TELEGRAM_ADMIN_IDS` will have access to the bot.
141
+
142
+ ### Bot Commands
143
+
144
+ - `/set_folder` - Set the folder for file uploads
145
+ - `/current_folder` - Check the current folder
146
+
147
+ ### Quick Demo
148
+
149
+ Bot Mode - Youtube Video Tutorial : https://youtu.be/XSeY2XcHdGI
150
+
151
+ #### Uploading Files
152
+
153
+ 1. Open your main bot in Telegram.
154
+ 2. Send or forward a file to this bot, and it will be uploaded. By default, the file will be uploaded to the root folder (home page).
155
+
156
+ #### Changing Folder for Uploading
157
+
158
+ 1. Send the `/set_folder` command and follow the instructions provided by the bot.
159
+
160
+ ## Important Posts Regarding TG Drive
161
+
162
+ Stay informed by joining our updates channel on Telegram: [@TechZBots](https://telegram.me/TechZBots). We post updates, guides, and tips about TG Drive there.
163
+
164
+ - https://telegram.me/TechZBots/891
165
+ - https://telegram.me/TechZBots/876
166
+ - https://telegram.me/TechZBots/874
167
+ - https://telegram.me/TechZBots/870
168
+
169
+ ## Contributing
170
+
171
+ Contributions are welcome! Fork the repository, make your changes, and create a pull request.
172
+
173
+ ## License
174
+
175
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
176
+
177
+ ## Support
178
+
179
+ For inquiries or support, join our [Telegram Support Group](https://telegram.me/TechZBots_Support) or email [techshreyash123@gmail.com](mailto:techshreyash123@gmail.com).
SETUP_SUMMARY.txt ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Hugging Face Space Configuration Summary
2
+ =========================================
3
+
4
+ REQUIRED for File Operations:
5
+ ------------------------------
6
+ API_ID=123456
7
+ API_HASH=your_api_hash
8
+ BOT_SESSIONS=bot.session
9
+ STORAGE_CHANNEL=-100123456789
10
+ DATABASE_BACKUP_MSG_ID=123
11
+ ADMIN_PASSWORD=your_password
12
+
13
+ NOTE: You need to upload cache/bot.session file to your Hugging Face Space
14
+ (since cache/ is gitignored, upload it manually or temporarily remove from .gitignore)
15
+
16
+ OPTIONAL - For Bot Mode (users can upload files via Telegram bot):
17
+ -------------------------------------------------------------------
18
+ MAIN_BOT_TOKEN=your_main_bot_token
19
+ TELEGRAM_ADMIN_IDS=123456789,987654321
20
+
21
+ NOTE: MAIN_BOT_TOKEN will automatically create main_bot.session file
22
+ You DON'T need to provide a session file for this - just the token!
23
+
24
+ OPTIONAL - For Premium (files > 2GB):
25
+ -------------------------------------
26
+ STRING_SESSIONS=your_string_session_here
27
+
cache/bot.session ADDED
Binary file (28.7 kB). View file
 
cache/main_bot.session ADDED
Binary file (28.7 kB). View file
 
config.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+ import os
3
+
4
+ # Load environment variables from the .env file, if present
5
+ load_dotenv()
6
+
7
+ # Telegram API credentials obtained from https://my.telegram.org/auth
8
+ API_ID = int(os.getenv("API_ID")) # Your Telegram API ID
9
+ API_HASH = os.getenv("API_HASH") # Your Telegram API Hash
10
+
11
+ # List of Telegram bot session file names (e.g., "bot.session,bot2.session")
12
+ # Session files should be in cache directory. Pyrogram will use them if they exist.
13
+ BOT_SESSIONS = os.getenv("BOT_SESSIONS", "").strip(", ").split(",")
14
+ BOT_SESSIONS = [session.strip() for session in BOT_SESSIONS if session.strip() != ""]
15
+
16
+ # List of Telegram bot tokens - used with session files (if session file doesn't exist, token creates it)
17
+ # If BOT_SESSIONS is empty, BOT_TOKENS will be used directly
18
+ BOT_TOKENS = os.getenv("BOT_TOKENS", "").strip(", ").split(",")
19
+ BOT_TOKENS = [token.strip() for token in BOT_TOKENS if token.strip() != ""]
20
+
21
+ # List of Telegram Account Pyrogram String Sessions used for file upload/download operations (optional)
22
+ STRING_SESSIONS = os.getenv("STRING_SESSIONS", "").strip(", ").split(",")
23
+ STRING_SESSIONS = [
24
+ session.strip() for session in STRING_SESSIONS if session.strip() != ""
25
+ ]
26
+
27
+ # Chat ID of the Telegram storage channel where files will be stored
28
+ STORAGE_CHANNEL = int(os.getenv("STORAGE_CHANNEL")) # Your storage channel's chat ID
29
+
30
+ # Message ID of a file in the storage channel used for storing database backups
31
+ # Telegram message IDs are 32-bit signed integers (max 2,147,483,647)
32
+ DATABASE_BACKUP_MSG_ID_STR = os.getenv("DATABASE_BACKUP_MSG_ID", "").strip()
33
+ if DATABASE_BACKUP_MSG_ID_STR:
34
+ try:
35
+ DATABASE_BACKUP_MSG_ID = int(DATABASE_BACKUP_MSG_ID_STR)
36
+ # Validate message ID is within Telegram's valid range (1 to 2^31-1)
37
+ if DATABASE_BACKUP_MSG_ID < 1:
38
+ raise ValueError(f"DATABASE_BACKUP_MSG_ID ({DATABASE_BACKUP_MSG_ID}) must be >= 1")
39
+ if DATABASE_BACKUP_MSG_ID > 2147483647:
40
+ raise ValueError(f"DATABASE_BACKUP_MSG_ID ({DATABASE_BACKUP_MSG_ID}) exceeds maximum (2147483647). Telegram message IDs are 32-bit signed integers.")
41
+ except ValueError as e:
42
+ if "invalid literal" in str(e) or "could not convert" in str(e):
43
+ raise ValueError(f"Invalid DATABASE_BACKUP_MSG_ID: '{DATABASE_BACKUP_MSG_ID_STR}'. Must be a valid integer.")
44
+ raise
45
+ else:
46
+ # Default to 1 if not set (will create new backup message on first upload)
47
+ DATABASE_BACKUP_MSG_ID = 1
48
+
49
+ # Password used to access the website's admin panel
50
+ ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin") # Default to "admin" if not set
51
+
52
+ # Determine the maximum file size (in bytes) allowed for uploading to Telegram
53
+ # String sessions support up to 4GB (if Premium), bot sessions/tokens limited to 2GB
54
+ if len(STRING_SESSIONS) > 0:
55
+ MAX_FILE_SIZE = 3.98 * 1024 * 1024 * 1024 # 4 GB in bytes (with Premium)
56
+ else:
57
+ MAX_FILE_SIZE = 1.98 * 1024 * 1024 * 1024 # 2 GB in bytes (bot sessions/tokens)
58
+
59
+ # Database backup interval in seconds. Backups will be sent to the storage channel at this interval
60
+ DATABASE_BACKUP_TIME = int(
61
+ os.getenv("DATABASE_BACKUP_TIME", 60)
62
+ ) # Default to 60 seconds
63
+
64
+ # Time delay in seconds before retrying after a Telegram API floodwait error
65
+ SLEEP_THRESHOLD = int(os.getenv("SLEEP_THRESHOLD", 60)) # Default to 60 seconds
66
+
67
+ # Domain to auto-ping and keep the website active
68
+ WEBSITE_URL = os.getenv("WEBSITE_URL", None)
69
+
70
+ # MongoDB configuration (used for fast message indexing/fetching and movie requests)
71
+ MONGODB_URI = os.getenv("MONGODB_URI", None)
72
+ MONGODB_DB_NAME = os.getenv("MONGODB_DB_NAME", "tgdrive")
73
+ MONGODB_COLLECTION = os.getenv("MONGODB_COLLECTION", "messages")
74
+ MONGODB_REQUESTS_COLLECTION = os.getenv("MONGODB_REQUESTS_COLLECTION", "movie_requests")
75
+
76
+ # TMDB configuration (for movie name and info fetch)
77
+ TMDB_API_KEY = os.getenv("TMDB_API_KEY", None)
78
+
79
+
80
+ # For Using TG Drive's Bot Mode
81
+
82
+ # Main Bot Token for TG Drive's Bot Mode
83
+ MAIN_BOT_TOKEN = os.getenv("MAIN_BOT_TOKEN", "")
84
+ if MAIN_BOT_TOKEN.strip() == "":
85
+ MAIN_BOT_TOKEN = None
86
+
87
+ # List of Telegram User IDs who have admin access to the bot mode
88
+ TELEGRAM_ADMIN_IDS = os.getenv("TELEGRAM_ADMIN_IDS", "").strip(", ").split(",")
89
+ TELEGRAM_ADMIN_IDS = [int(id) for id in TELEGRAM_ADMIN_IDS if id.strip() != ""]
docker_start.sh ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ docker stop tgdrive
2
+ docker build -t tgdrive .
3
+ docker run -d --name tgdrive -p 80:80 tgdrive
main.py ADDED
@@ -0,0 +1,528 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from utils.downloader import (
2
+ download_file,
3
+ get_file_info_from_url,
4
+ )
5
+ import asyncio
6
+ from pathlib import Path
7
+ from contextlib import asynccontextmanager
8
+ import aiofiles
9
+ from fastapi import FastAPI, HTTPException, Request, File, UploadFile, Form, Response
10
+ from fastapi.responses import FileResponse, JSONResponse
11
+ from config import ADMIN_PASSWORD, MAX_FILE_SIZE, STORAGE_CHANNEL, TMDB_API_KEY
12
+ from utils.clients import initialize_clients
13
+ from utils.directoryHandler import getRandomID
14
+ from utils.extra import auto_ping_website, convert_class_to_dict, reset_cache_dir
15
+ from utils.streamer import media_streamer
16
+ from utils.uploader import start_file_uploader
17
+ from utils.logger import Logger
18
+ import urllib.parse
19
+ from datetime import datetime
20
+ import aiohttp
21
+
22
+ from utils.movie_requests import (
23
+ save_movie_request,
24
+ MovieRequestsMongoNotConfigured,
25
+ )
26
+
27
+
28
+ # Startup Event
29
+ @asynccontextmanager
30
+ async def lifespan(app: FastAPI):
31
+ # Reset the cache directory, delete cache files
32
+ reset_cache_dir()
33
+
34
+ # Initialize the clients
35
+ clients_initialized = await initialize_clients()
36
+
37
+ # If clients failed to initialize, initialize DRIVE_DATA in offline mode
38
+ if not clients_initialized:
39
+ logger.warning("No Telegram clients available - Initializing in OFFLINE MODE")
40
+ from utils.directoryHandler import initDriveDataWithoutClients
41
+ await initDriveDataWithoutClients()
42
+ logger.warning("✅ Website is running in OFFLINE MODE - Telegram features are disabled")
43
+ logger.info("✅ Website UI is available - File operations requiring Telegram will show errors")
44
+
45
+ # Start the website auto ping task
46
+ asyncio.create_task(auto_ping_website())
47
+
48
+ yield
49
+
50
+
51
+ app = FastAPI(docs_url=None, redoc_url=None, lifespan=lifespan)
52
+ logger = Logger(__name__)
53
+
54
+
55
+ @app.get("/")
56
+ async def home_page():
57
+ return FileResponse("website/home.html")
58
+
59
+
60
+ @app.get("/stream")
61
+ async def home_page():
62
+ return FileResponse("website/VideoPlayer.html")
63
+
64
+
65
+ @app.get("/static/{file_path:path}")
66
+ async def static_files(file_path):
67
+ if "apiHandler.js" in file_path:
68
+ with open(Path("website/static/js/apiHandler.js")) as f:
69
+ content = f.read()
70
+ content = content.replace("MAX_FILE_SIZE__SDGJDG", str(MAX_FILE_SIZE))
71
+ return Response(content=content, media_type="application/javascript")
72
+ return FileResponse(f"website/static/{file_path}")
73
+
74
+
75
+ @app.get("/file")
76
+ async def dl_file(request: Request):
77
+ from utils.directoryHandler import DRIVE_DATA
78
+ from utils.clients import has_clients
79
+
80
+ if not has_clients():
81
+ raise HTTPException(status_code=503, detail="Telegram clients not available - Service running in offline mode")
82
+
83
+ path = request.query_params["path"]
84
+ file = DRIVE_DATA.get_file(path)
85
+ return await media_streamer(STORAGE_CHANNEL, file.file_id, file.name, request)
86
+
87
+
88
+ # Api Routes
89
+
90
+
91
+ @app.post("/api/checkPassword")
92
+ async def check_password(request: Request):
93
+ data = await request.json()
94
+ if data["pass"] == ADMIN_PASSWORD:
95
+ return JSONResponse({"status": "ok"})
96
+ return JSONResponse({"status": "Invalid password"})
97
+
98
+
99
+ @app.post("/api/createNewFolder")
100
+ async def api_new_folder(request: Request):
101
+ from utils.directoryHandler import DRIVE_DATA
102
+
103
+ data = await request.json()
104
+
105
+ if data["password"] != ADMIN_PASSWORD:
106
+ return JSONResponse({"status": "Invalid password"})
107
+
108
+ logger.info(f"createNewFolder {data}")
109
+ folder_data = DRIVE_DATA.get_directory(data["path"]).contents
110
+ for id in folder_data:
111
+ f = folder_data[id]
112
+ if f.type == "folder":
113
+ if f.name == data["name"]:
114
+ return JSONResponse(
115
+ {
116
+ "status": "Folder with the name already exist in current directory"
117
+ }
118
+ )
119
+
120
+ DRIVE_DATA.new_folder(data["path"], data["name"])
121
+ return JSONResponse({"status": "ok"})
122
+
123
+
124
+ @app.post("/api/getDirectory")
125
+ async def api_get_directory(request: Request):
126
+ from utils.directoryHandler import DRIVE_DATA
127
+
128
+ data = await request.json()
129
+
130
+ if data["password"] == ADMIN_PASSWORD:
131
+ is_admin = True
132
+ else:
133
+ is_admin = False
134
+
135
+ auth = data.get("auth")
136
+
137
+ logger.info(f"getFolder {data}")
138
+
139
+ if data["path"] == "/trash":
140
+ data = {"contents": DRIVE_DATA.get_trashed_files_folders()}
141
+ folder_data = convert_class_to_dict(data, isObject=False, showtrash=True)
142
+
143
+ elif "/search_" in data["path"]:
144
+ query = urllib.parse.unquote(data["path"].split("_", 1)[1])
145
+ print(query)
146
+ data = {"contents": DRIVE_DATA.search_file_folder(query)}
147
+ print(data)
148
+ folder_data = convert_class_to_dict(data, isObject=False, showtrash=False)
149
+ print(folder_data)
150
+
151
+ elif "/share_" in data["path"]:
152
+ path = data["path"].split("_", 1)[1]
153
+ folder_data, auth_home_path = DRIVE_DATA.get_directory(path, is_admin, auth)
154
+ auth_home_path= auth_home_path.replace("//", "/") if auth_home_path else None
155
+ folder_data = convert_class_to_dict(folder_data, isObject=True, showtrash=False)
156
+ return JSONResponse(
157
+ {"status": "ok", "data": folder_data, "auth_home_path": auth_home_path}
158
+ )
159
+
160
+ else:
161
+ folder_data = DRIVE_DATA.get_directory(data["path"])
162
+ folder_data = convert_class_to_dict(folder_data, isObject=True, showtrash=False)
163
+ return JSONResponse({"status": "ok", "data": folder_data, "auth_home_path": None})
164
+
165
+
166
+ SAVE_PROGRESS = {}
167
+
168
+
169
+ @app.post("/api/upload")
170
+ async def upload_file(
171
+ file: UploadFile = File(...),
172
+ path: str = Form(...),
173
+ password: str = Form(...),
174
+ id: str = Form(...),
175
+ total_size: str = Form(...),
176
+ ):
177
+ global SAVE_PROGRESS
178
+ from utils.clients import has_clients
179
+
180
+ if password != ADMIN_PASSWORD:
181
+ return JSONResponse({"status": "Invalid password"})
182
+
183
+ if not has_clients():
184
+ return JSONResponse({"status": "Telegram clients not available - Service running in offline mode"})
185
+
186
+ total_size = int(total_size)
187
+ SAVE_PROGRESS[id] = ("running", 0, total_size)
188
+
189
+ ext = file.filename.lower().split(".")[-1]
190
+
191
+ cache_dir = Path("./cache")
192
+ cache_dir.mkdir(parents=True, exist_ok=True)
193
+ file_location = cache_dir / f"{id}.{ext}"
194
+
195
+ file_size = 0
196
+
197
+ async with aiofiles.open(file_location, "wb") as buffer:
198
+ while chunk := await file.read(1024 * 1024): # Read file in chunks of 1MB
199
+ SAVE_PROGRESS[id] = ("running", file_size, total_size)
200
+ file_size += len(chunk)
201
+ if file_size > MAX_FILE_SIZE:
202
+ await buffer.close()
203
+ file_location.unlink() # Delete the partially written file
204
+ raise HTTPException(
205
+ status_code=400,
206
+ detail=f"File size exceeds {MAX_FILE_SIZE} bytes limit",
207
+ )
208
+ await buffer.write(chunk)
209
+
210
+ SAVE_PROGRESS[id] = ("completed", file_size, file_size)
211
+
212
+ asyncio.create_task(
213
+ start_file_uploader(file_location, id, path, file.filename, file_size)
214
+ )
215
+
216
+ return JSONResponse({"id": id, "status": "ok"})
217
+
218
+
219
+ @app.post("/api/getSaveProgress")
220
+ async def get_save_progress(request: Request):
221
+ global SAVE_PROGRESS
222
+
223
+ data = await request.json()
224
+
225
+ if data["password"] != ADMIN_PASSWORD:
226
+ return JSONResponse({"status": "Invalid password"})
227
+
228
+ logger.info(f"getUploadProgress {data}")
229
+ try:
230
+ progress = SAVE_PROGRESS[data["id"]]
231
+ return JSONResponse({"status": "ok", "data": progress})
232
+ except:
233
+ return JSONResponse({"status": "not found"})
234
+
235
+
236
+ @app.post("/api/getUploadProgress")
237
+ async def get_upload_progress(request: Request):
238
+ from utils.uploader import PROGRESS_CACHE
239
+
240
+ data = await request.json()
241
+
242
+ if data["password"] != ADMIN_PASSWORD:
243
+ return JSONResponse({"status": "Invalid password"})
244
+
245
+ logger.info(f"getUploadProgress {data}")
246
+
247
+ try:
248
+ progress = PROGRESS_CACHE[data["id"]]
249
+ return JSONResponse({"status": "ok", "data": progress})
250
+ except:
251
+ return JSONResponse({"status": "not found"})
252
+
253
+
254
+ @app.post("/api/cancelUpload")
255
+ async def cancel_upload(request: Request):
256
+ from utils.uploader import STOP_TRANSMISSION
257
+ from utils.downloader import STOP_DOWNLOAD
258
+
259
+ data = await request.json()
260
+
261
+ if data["password"] != ADMIN_PASSWORD:
262
+ return JSONResponse({"status": "Invalid password"})
263
+
264
+ logger.info(f"cancelUpload {data}")
265
+ STOP_TRANSMISSION.append(data["id"])
266
+ STOP_DOWNLOAD.append(data["id"])
267
+ return JSONResponse({"status": "ok"})
268
+
269
+
270
+ @app.post("/api/renameFileFolder")
271
+ async def rename_file_folder(request: Request):
272
+ from utils.directoryHandler import DRIVE_DATA
273
+
274
+ data = await request.json()
275
+
276
+ if data["password"] != ADMIN_PASSWORD:
277
+ return JSONResponse({"status": "Invalid password"})
278
+
279
+ logger.info(f"renameFileFolder {data}")
280
+ DRIVE_DATA.rename_file_folder(data["path"], data["name"])
281
+ return JSONResponse({"status": "ok"})
282
+
283
+
284
+ @app.post("/api/trashFileFolder")
285
+ async def trash_file_folder(request: Request):
286
+ from utils.directoryHandler import DRIVE_DATA
287
+
288
+ data = await request.json()
289
+
290
+ if data["password"] != ADMIN_PASSWORD:
291
+ return JSONResponse({"status": "Invalid password"})
292
+
293
+ logger.info(f"trashFileFolder {data}")
294
+ DRIVE_DATA.trash_file_folder(data["path"], data["trash"])
295
+ return JSONResponse({"status": "ok"})
296
+
297
+
298
+ @app.post("/api/deleteFileFolder")
299
+ async def delete_file_folder(request: Request):
300
+ from utils.directoryHandler import DRIVE_DATA
301
+
302
+ data = await request.json()
303
+
304
+ if data["password"] != ADMIN_PASSWORD:
305
+ return JSONResponse({"status": "Invalid password"})
306
+
307
+ logger.info(f"deleteFileFolder {data}")
308
+ DRIVE_DATA.delete_file_folder(data["path"])
309
+ return JSONResponse({"status": "ok"})
310
+
311
+
312
+ @app.post("/api/getFileInfoFromUrl")
313
+ async def getFileInfoFromUrl(request: Request):
314
+
315
+ data = await request.json()
316
+
317
+ if data["password"] != ADMIN_PASSWORD:
318
+ return JSONResponse({"status": "Invalid password"})
319
+
320
+ logger.info(f"getFileInfoFromUrl {data}")
321
+ try:
322
+ file_info = await get_file_info_from_url(data["url"])
323
+ return JSONResponse({"status": "ok", "data": file_info})
324
+ except Exception as e:
325
+ return JSONResponse({"status": str(e)})
326
+
327
+
328
+ @app.post("/api/startFileDownloadFromUrl")
329
+ async def startFileDownloadFromUrl(request: Request):
330
+ from utils.clients import has_clients
331
+
332
+ data = await request.json()
333
+
334
+ if data["password"] != ADMIN_PASSWORD:
335
+ return JSONResponse({"status": "Invalid password"})
336
+
337
+ if not has_clients():
338
+ return JSONResponse({"status": "Telegram clients not available - Service running in offline mode"})
339
+
340
+ logger.info(f"startFileDownloadFromUrl {data}")
341
+ try:
342
+ id = getRandomID()
343
+ asyncio.create_task(
344
+ download_file(data["url"], id, data["path"], data["filename"], data["singleThreaded"])
345
+ )
346
+ return JSONResponse({"status": "ok", "id": id})
347
+ except Exception as e:
348
+ return JSONResponse({"status": str(e)})
349
+
350
+
351
+ @app.post("/api/getFileDownloadProgress")
352
+ async def getFileDownloadProgress(request: Request):
353
+ from utils.downloader import DOWNLOAD_PROGRESS
354
+
355
+ data = await request.json()
356
+
357
+ if data["password"] != ADMIN_PASSWORD:
358
+ return JSONResponse({"status": "Invalid password"})
359
+
360
+ logger.info(f"getFileDownloadProgress {data}")
361
+
362
+ try:
363
+ progress = DOWNLOAD_PROGRESS[data["id"]]
364
+ return JSONResponse({"status": "ok", "data": progress})
365
+ except:
366
+ return JSONResponse({"status": "not found"})
367
+
368
+
369
+ @app.post("/api/getFolderShareAuth")
370
+ async def getFolderShareAuth(request: Request):
371
+ from utils.directoryHandler import DRIVE_DATA
372
+
373
+ data = await request.json()
374
+
375
+ if data["password"] != ADMIN_PASSWORD:
376
+ return JSONResponse({"status": "Invalid password"})
377
+
378
+ logger.info(f"getFolderShareAuth {data}")
379
+
380
+ try:
381
+ auth = DRIVE_DATA.get_folder_auth(data["path"])
382
+ return JSONResponse({"status": "ok", "auth": auth})
383
+ except:
384
+ return JSONResponse({"status": "not found"})
385
+
386
+
387
+ def _find_movie_file(title: str, year: str):
388
+ """
389
+ Find a movie file in DRIVE_DATA by matching title and year in the filename.
390
+ Returns a dict with path, is_video, and name or None if not found.
391
+ """
392
+ from utils.directoryHandler import DRIVE_DATA
393
+
394
+ if not DRIVE_DATA:
395
+ return None
396
+
397
+ query = title.strip()
398
+ year_str = str(year).strip()
399
+
400
+ # Use built-in search to narrow down candidates
401
+ search_results = DRIVE_DATA.search_file_folder(query)
402
+ candidates = []
403
+
404
+ for item in search_results.values():
405
+ if getattr(item, "type", None) != "file":
406
+ continue
407
+ name_lower = item.name.lower()
408
+ if query.lower() in name_lower and year_str in item.name:
409
+ candidates.append(item)
410
+
411
+ if not candidates:
412
+ return None
413
+
414
+ # Pick the first candidate (could be improved with better ranking)
415
+ file = candidates[0]
416
+ full_path = f"{file.path.rstrip('/')}/{file.id}"
417
+ lower_name = file.name.lower()
418
+ is_video = lower_name.endswith(
419
+ (".mp4", ".mkv", ".webm", ".mov", ".avi", ".ts", ".ogv")
420
+ )
421
+
422
+ return {
423
+ "path": full_path,
424
+ "is_video": is_video,
425
+ "name": file.name,
426
+ }
427
+
428
+
429
+ @app.post("/api/tmdbSearchMovie")
430
+ async def tmdb_search_movie(request: Request):
431
+ """
432
+ Search movie info from TMDB using title and optional year.
433
+ """
434
+ data = await request.json()
435
+
436
+ if data.get("password") != ADMIN_PASSWORD:
437
+ return JSONResponse({"status": "Invalid password"})
438
+
439
+ if not TMDB_API_KEY:
440
+ return JSONResponse({"status": "TMDB not configured"})
441
+
442
+ title = data.get("title", "").strip()
443
+ year = data.get("year")
444
+
445
+ if not title:
446
+ return JSONResponse({"status": "Title is required"})
447
+
448
+ params = {
449
+ "api_key": TMDB_API_KEY,
450
+ "query": title,
451
+ "include_adult": False,
452
+ }
453
+ if year:
454
+ params["year"] = year
455
+
456
+ url = "https://api.themoviedb.org/3/search/movie"
457
+
458
+ try:
459
+ async with aiohttp.ClientSession() as session:
460
+ async with session.get(url, params=params) as resp:
461
+ if resp.status != 200:
462
+ return JSONResponse(
463
+ {"status": f"TMDB error: HTTP {resp.status}"}
464
+ )
465
+ payload = await resp.json()
466
+ except Exception as e:
467
+ return JSONResponse({"status": f"TMDB request failed: {e}"})
468
+
469
+ results = payload.get("results", [])
470
+ if not results:
471
+ return JSONResponse({"status": "no_results"})
472
+
473
+ # Return top result only to keep it simple
474
+ top = results[0]
475
+ movie_info = {
476
+ "id": top.get("id"),
477
+ "title": top.get("title"),
478
+ "original_title": top.get("original_title"),
479
+ "overview": top.get("overview"),
480
+ "release_date": top.get("release_date"),
481
+ "year": (top.get("release_date") or "")[:4],
482
+ "poster_path": top.get("poster_path"),
483
+ }
484
+
485
+ return JSONResponse({"status": "ok", "data": movie_info})
486
+
487
+
488
+ @app.post("/api/requestMovie")
489
+ async def request_movie(request: Request):
490
+ """
491
+ Request a movie by title and year.
492
+ - Saves the request in MongoDB.
493
+ - Checks if a matching movie file is available in DRIVE_DATA.
494
+ - Returns shareable link path info if found.
495
+ """
496
+ data = await request.json()
497
+
498
+ if data.get("password") != ADMIN_PASSWORD:
499
+ return JSONResponse({"status": "Invalid password"})
500
+
501
+ title = (data.get("title") or "").strip()
502
+ year = (data.get("year") or "").strip()
503
+
504
+ if not title or not year:
505
+ return JSONResponse({"status": "Title and year are required"})
506
+
507
+ # Find movie in drive data
508
+ file_info = _find_movie_file(title, year)
509
+ available = file_info is not None
510
+
511
+ # Save request in MongoDB (if configured)
512
+ try:
513
+ await save_movie_request(title, year, available, file_info=file_info)
514
+ except MovieRequestsMongoNotConfigured:
515
+ # Mongo not configured for movie requests - just skip saving
516
+ logger.warning("MongoDB not configured for movie requests - skipping save")
517
+ except Exception as e:
518
+ logger.error(f"Error saving movie request: {e}")
519
+
520
+ if not available:
521
+ return JSONResponse({"status": "not_found"})
522
+
523
+ return JSONResponse(
524
+ {
525
+ "status": "available",
526
+ "file": file_info,
527
+ }
528
+ )
render.yaml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ # A Docker web service
3
+ - type: web
4
+ name: tgdrive
5
+ repo: https://github.com/TechShreyash/TGDrive
6
+ runtime: python
7
+ branch: main
8
+ plan: free
9
+ autoDeploy: false
10
+ buildCommand: pip install -r requirements.txt
11
+ startCommand: uvicorn main:app --host 0.0.0.0 --port $PORT
12
+ envVars:
13
+ - key: ADMIN_PASSWORD
14
+ sync: false
15
+ - key: API_ID
16
+ sync: false
17
+ - key: API_HASH
18
+ sync: false
19
+ - key: BOT_TOKENS
20
+ sync: false
21
+ - key: STORAGE_CHANNEL
22
+ sync: false
23
+ - key: DATABASE_BACKUP_MSG_ID
24
+ sync: false
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ https://github.com/KurimuzonAkuma/pyrogram/archive/dev.zip
2
+ tgcrypto
3
+ uvicorn
4
+ fastapi[all]
5
+ python-dotenv
6
+ aiofiles
7
+ aiohttp
8
+ curl_cffi
9
+ techzdl>=1.2.6
10
+ tqdm
11
+ dill
12
+ pyromod
13
+ motor
runtime.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ python-3.11
sample.env ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Required Vars
2
+
3
+ API_ID=123456
4
+ API_HASH=dagsjdhgjfsahgjfh
5
+ BOT_TOKENS=21413535:gkdshajfhjfakhjf
6
+ STORAGE_CHANNEL=-100123456789
7
+ DATABASE_BACKUP_MSG_ID=123
8
+
9
+ # Optional: Bot session file names (must match BOT_TOKENS count)
10
+ # Place session files in cache/ directory. Pyrogram will use them if they exist.
11
+ # BOT_SESSIONS=bot.session,bot2.session
12
+
13
+ # Optional: String sessions for Premium accounts (files > 2GB)
14
+ # STRING_SESSIONS=1BVtsOMwBu5...your_session_string_here...
15
+
16
+ # Note:
17
+ # - BOT_TOKENS is required (Pyrogram needs token for bot clients)
18
+ # - BOT_SESSIONS is optional - if provided, Pyrogram will use those session file names
19
+ # - If session file exists, it will be used (faster connection)
20
+ # - If session file doesn't exist, Pyrogram will create it
start_main.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ import os
2
+
3
+ os.system("uvicorn main:app --reload")
utils/bot_mode.py ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from pyrogram import Client, filters
3
+ import pyromod
4
+ from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
5
+ import config
6
+ from utils.logger import Logger
7
+ from pathlib import Path
8
+
9
+ from utils.mongo_indexer import index_channel_messages, MongoNotConfiguredError
10
+
11
+ logger = Logger(__name__)
12
+
13
+ START_CMD = """🚀 **Welcome To TG Drive's Bot Mode**
14
+
15
+ You can use this bot to upload files to your TG Drive website directly instead of doing it from website.
16
+
17
+ 🗄 **Commands:**
18
+ /set_folder - Set folder for file uploads
19
+ /current_folder - Check current folder
20
+
21
+ 📤 **How To Upload Files:** Send a file to this bot and it will be uploaded to your TG Drive website. You can also set a folder for file uploads using /set_folder command.
22
+
23
+ Read more about [TG Drive's Bot Mode](https://github.com/TechShreyash/TGDrive#tg-drives-bot-mode)
24
+ """
25
+
26
+ SET_FOLDER_PATH_CACHE = {} # Cache to store folder path for each folder id
27
+ DRIVE_DATA = None
28
+ BOT_MODE = None
29
+
30
+ session_cache_path = Path(f"./cache")
31
+ session_cache_path.parent.mkdir(parents=True, exist_ok=True)
32
+
33
+ main_bot = Client(
34
+ name="main_bot",
35
+ api_id=config.API_ID,
36
+ api_hash=config.API_HASH,
37
+ bot_token=config.MAIN_BOT_TOKEN,
38
+ sleep_threshold=config.SLEEP_THRESHOLD,
39
+ workdir=session_cache_path,
40
+ )
41
+
42
+
43
+ @main_bot.on_message(
44
+ filters.command(["start", "help"])
45
+ & filters.private
46
+ & filters.user(config.TELEGRAM_ADMIN_IDS),
47
+ )
48
+ async def start_handler(client: Client, message: Message):
49
+ await message.reply_text(START_CMD)
50
+
51
+
52
+ @main_bot.on_message(
53
+ filters.command("index")
54
+ & filters.private
55
+ & filters.user(config.TELEGRAM_ADMIN_IDS),
56
+ )
57
+ async def index_handler(client: Client, message: Message):
58
+ """
59
+ /index <channel_id or @username>
60
+
61
+ Index all messages of the given channel/chat into MongoDB for fast fetching.
62
+ """
63
+ if len(message.command) < 2:
64
+ await message.reply_text("Usage: /index <channel_id or @username>")
65
+ return
66
+
67
+ raw_target = message.command[1].strip()
68
+
69
+ # Try to parse as integer chat ID, otherwise use raw (could be @username or link)
70
+ try:
71
+ target = int(raw_target)
72
+ except ValueError:
73
+ target = raw_target
74
+
75
+ try:
76
+ await message.reply_text(
77
+ f"Started indexing messages for `{raw_target}`.\n\n"
78
+ "This may take a while for large channels. You'll get a message when it's done.",
79
+ quote=True,
80
+ )
81
+
82
+ async def status_cb(text: str):
83
+ try:
84
+ await message.reply_text(text)
85
+ except Exception:
86
+ # Ignore failures (e.g., rate limits or message delete)
87
+ pass
88
+
89
+ # Run indexing in a background task so the bot stays responsive
90
+ async def _run():
91
+ try:
92
+ total = await index_channel_messages(client, target, status_callback=status_cb)
93
+ await message.reply_text(
94
+ f"✅ Indexing completed for `{raw_target}`.\nTotal messages indexed: **{total}**"
95
+ )
96
+ except MongoNotConfiguredError as e:
97
+ await message.reply_text(
98
+ "MongoDB is not configured.\n\n"
99
+ "Please set `MONGODB_URI` (and optionally `MONGODB_DB_NAME`, "
100
+ "`MONGODB_COLLECTION`) in your environment.",
101
+ )
102
+ except Exception as e:
103
+ await message.reply_text(f"❌ Failed to index channel `{raw_target}`.\nError: `{e}`")
104
+
105
+ asyncio.create_task(_run())
106
+
107
+ except MongoNotConfiguredError:
108
+ await message.reply_text(
109
+ "MongoDB is not configured.\n\n"
110
+ "Please set `MONGODB_URI` (and optionally `MONGODB_DB_NAME`, "
111
+ "`MONGODB_COLLECTION`) in your environment.",
112
+ )
113
+ except Exception as e:
114
+ await message.reply_text(f"❌ Failed to start indexing.\nError: `{e}`")
115
+
116
+
117
+ @main_bot.on_message(
118
+ filters.command("set_folder")
119
+ & filters.private
120
+ & filters.user(config.TELEGRAM_ADMIN_IDS),
121
+ )
122
+ async def set_folder_handler(client: Client, message: Message):
123
+ global SET_FOLDER_PATH_CACHE, DRIVE_DATA
124
+
125
+ while True:
126
+ try:
127
+ folder_name = await client.ask(
128
+ message.chat.id,
129
+ "Send the folder name where you want to upload files\n\n/cancel to cancel",
130
+ timeout=60,
131
+ filters=filters.text,
132
+ )
133
+ except asyncio.TimeoutError:
134
+ await message.reply_text("Timeout\n\nUse /set_folder to set folder again")
135
+ return
136
+
137
+ if folder_name.text.lower() == "/cancel":
138
+ await message.reply_text("Cancelled")
139
+ return
140
+
141
+ folder_name = folder_name.text.strip()
142
+ search_result = DRIVE_DATA.search_file_folder(folder_name)
143
+
144
+ # Get folders from search result
145
+ folders = {}
146
+ for item in search_result.values():
147
+ if item.type == "folder":
148
+ folders[item.id] = item
149
+
150
+ if len(folders) == 0:
151
+ await message.reply_text(f"No Folder found with name {folder_name}")
152
+ else:
153
+ break
154
+
155
+ buttons = []
156
+ folder_cache = {}
157
+ folder_cache_id = len(SET_FOLDER_PATH_CACHE) + 1
158
+
159
+ for folder in search_result.values():
160
+ path = folder.path.strip("/")
161
+ folder_path = "/" + ("/" + path + "/" + folder.id).strip("/")
162
+ folder_cache[folder.id] = (folder_path, folder.name)
163
+ buttons.append(
164
+ [
165
+ InlineKeyboardButton(
166
+ folder.name,
167
+ callback_data=f"set_folder_{folder_cache_id}_{folder.id}",
168
+ )
169
+ ]
170
+ )
171
+ SET_FOLDER_PATH_CACHE[folder_cache_id] = folder_cache
172
+
173
+ await message.reply_text(
174
+ "Select the folder where you want to upload files",
175
+ reply_markup=InlineKeyboardMarkup(buttons),
176
+ )
177
+
178
+
179
+ @main_bot.on_callback_query(
180
+ filters.user(config.TELEGRAM_ADMIN_IDS) & filters.regex(r"set_folder_")
181
+ )
182
+ async def set_folder_callback(client: Client, callback_query: Message):
183
+ global SET_FOLDER_PATH_CACHE, BOT_MODE
184
+
185
+ folder_cache_id, folder_id = callback_query.data.split("_")[2:]
186
+
187
+ folder_path_cache = SET_FOLDER_PATH_CACHE.get(int(folder_cache_id))
188
+ if folder_path_cache is None:
189
+ await callback_query.answer("Request Expired, Send /set_folder again")
190
+ await callback_query.message.delete()
191
+ return
192
+
193
+ folder_path, name = folder_path_cache.get(folder_id)
194
+ del SET_FOLDER_PATH_CACHE[int(folder_cache_id)]
195
+ BOT_MODE.set_folder(folder_path, name)
196
+
197
+ await callback_query.answer(f"Folder Set Successfully To : {name}")
198
+ await callback_query.message.edit(
199
+ f"Folder Set Successfully To : {name}\n\nNow you can send / forward files to me and it will be uploaded to this folder."
200
+ )
201
+
202
+
203
+ @main_bot.on_message(
204
+ filters.command("current_folder")
205
+ & filters.private
206
+ & filters.user(config.TELEGRAM_ADMIN_IDS),
207
+ )
208
+ async def current_folder_handler(client: Client, message: Message):
209
+ global BOT_MODE
210
+
211
+ await message.reply_text(f"Current Folder: {BOT_MODE.current_folder_name}")
212
+
213
+
214
+ # Handling when any file is sent to the bot
215
+ @main_bot.on_message(
216
+ filters.private
217
+ & filters.user(config.TELEGRAM_ADMIN_IDS)
218
+ & (
219
+ filters.document
220
+ | filters.video
221
+ | filters.audio
222
+ | filters.photo
223
+ | filters.sticker
224
+ )
225
+ )
226
+ async def file_handler(client: Client, message: Message):
227
+ global BOT_MODE, DRIVE_DATA
228
+
229
+ copied_message = await message.copy(config.STORAGE_CHANNEL)
230
+ file = (
231
+ copied_message.document
232
+ or copied_message.video
233
+ or copied_message.audio
234
+ or copied_message.photo
235
+ or copied_message.sticker
236
+ )
237
+
238
+ DRIVE_DATA.new_file(
239
+ BOT_MODE.current_folder,
240
+ file.file_name,
241
+ copied_message.id,
242
+ file.file_size,
243
+ )
244
+
245
+ await message.reply_text(
246
+ f"""✅ File Uploaded Successfully To Your TG Drive Website
247
+
248
+ **File Name:** {file.file_name}
249
+ **Folder:** {BOT_MODE.current_folder_name}
250
+ """
251
+ )
252
+
253
+
254
+ async def start_bot_mode(d, b):
255
+ global DRIVE_DATA, BOT_MODE
256
+ DRIVE_DATA = d
257
+ BOT_MODE = b
258
+
259
+ logger.info("Starting Main Bot")
260
+ await main_bot.start()
261
+
262
+ await main_bot.send_message(
263
+ config.STORAGE_CHANNEL, "Main Bot Started -> TG Drive's Bot Mode Enabled"
264
+ )
265
+ logger.info("Main Bot Started")
266
+ logger.info("TG Drive's Bot Mode Enabled")
utils/clients.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio, config
2
+ import traceback
3
+ from pathlib import Path
4
+ from pyrogram import Client
5
+ from pyrogram.errors import AuthKeyDuplicated
6
+ from utils.directoryHandler import backup_drive_data, loadDriveData
7
+ from utils.logger import Logger
8
+
9
+ logger = Logger(__name__)
10
+
11
+ multi_clients = {}
12
+ premium_clients = {}
13
+ work_loads = {}
14
+ premium_work_loads = {}
15
+ main_bot = None
16
+
17
+
18
+ async def initialize_clients():
19
+ global multi_clients, work_loads, premium_clients, premium_work_loads
20
+ logger.info("Initializing Clients")
21
+
22
+ session_cache_path = Path(f"./cache")
23
+ session_cache_path.parent.mkdir(parents=True, exist_ok=True)
24
+
25
+ # Log configuration for debugging
26
+ logger.info(f"BOT_SESSIONS configured: {len(config.BOT_SESSIONS)} session(s)")
27
+ logger.info(f"BOT_TOKENS configured: {len(config.BOT_TOKENS)} token(s)")
28
+ if config.BOT_SESSIONS:
29
+ logger.info(f"BOT_SESSIONS: {config.BOT_SESSIONS}")
30
+ if config.BOT_TOKENS:
31
+ logger.info(f"BOT_TOKENS: {len(config.BOT_TOKENS[0]) if config.BOT_TOKENS else 0} chars (hidden)")
32
+
33
+ # Use BOT_SESSIONS as Client names (if provided), otherwise use client_id
34
+ # Match BOT_SESSIONS with BOT_TOKENS
35
+ all_bot_sessions = dict((i, s) for i, s in enumerate(config.BOT_SESSIONS, start=1))
36
+ all_tokens = dict((i, t) for i, t in enumerate(config.BOT_TOKENS, start=1))
37
+
38
+ # Check if BOT_TOKENS is empty
39
+ if not all_tokens:
40
+ logger.error("BOT_TOKENS is empty! You must provide BOT_TOKENS in environment variables.")
41
+ logger.error("BOT_SESSIONS alone cannot work - Pyrogram requires bot_token for bot clients.")
42
+ return False
43
+
44
+ # Match sessions with tokens
45
+ if all_bot_sessions:
46
+ if len(all_bot_sessions) != len(all_tokens):
47
+ logger.warning(f"BOT_SESSIONS count ({len(all_bot_sessions)}) doesn't match BOT_TOKENS count ({len(all_tokens)})")
48
+ min_len = min(len(all_bot_sessions), len(all_tokens))
49
+ all_bot_sessions = dict(list(all_bot_sessions.items())[:min_len])
50
+ all_tokens = dict(list(all_tokens.items())[:min_len])
51
+ # Zip sessions and tokens
52
+ bot_configs = dict((i, (session, token)) for i, (session, token) in
53
+ enumerate(zip(all_bot_sessions.values(), all_tokens.values()), start=1))
54
+ else:
55
+ # No BOT_SESSIONS, use client_id as name
56
+ bot_configs = dict((i, (None, token)) for i, token in all_tokens.items())
57
+
58
+ all_string_sessions = dict(
59
+ (i, s) for i, s in enumerate(config.STRING_SESSIONS, start=len(bot_configs) + 1)
60
+ )
61
+
62
+ async def start_client(client_id, session_name_and_token, type):
63
+ try:
64
+ logger.info(f"Starting - {type.title()} Client {client_id}")
65
+
66
+ if type == "bot":
67
+ # Get session name and token
68
+ session_name, token = session_name_and_token
69
+
70
+ # Use session name as Client name (remove .session extension if present)
71
+ # If no session name, use client_id
72
+ if session_name:
73
+ client_name = session_name.replace(".session", "")
74
+ else:
75
+ client_name = str(client_id)
76
+
77
+ # Pyrogram will use existing session file if it exists in cache/, or create new one
78
+ client = Client(
79
+ name=client_name,
80
+ api_id=config.API_ID,
81
+ api_hash=config.API_HASH,
82
+ bot_token=token,
83
+ workdir=session_cache_path,
84
+ )
85
+ client.loop = asyncio.get_running_loop()
86
+ try:
87
+ await client.start()
88
+ await client.send_message(
89
+ config.STORAGE_CHANNEL,
90
+ f"Started - Bot Client {client_id}",
91
+ )
92
+ multi_clients[client_id] = client
93
+ work_loads[client_id] = 0
94
+ except AuthKeyDuplicated as e:
95
+ # Delete the invalidated session file and mark it as invalidated
96
+ session_file = None
97
+ try:
98
+ session_file = session_cache_path / f"{client_name}.session"
99
+ if session_file.exists():
100
+ session_file.unlink()
101
+ logger.info(f"Deleted invalidated session file: {session_file}")
102
+ # Create a marker file so we don't restore this session on next restart
103
+ invalidated_marker = session_cache_path / f"{client_name}.session.invalidated"
104
+ invalidated_marker.touch()
105
+ logger.info(f"Marked session file as invalidated: {invalidated_marker}")
106
+ except Exception as cleanup_error:
107
+ logger.error(f"Failed to delete session file: {cleanup_error}")
108
+
109
+ error_msg = (
110
+ f"\n{'='*60}\n"
111
+ f"AUTH_KEY_DUPLICATED for Bot Client {client_id}\n"
112
+ f"{'='*60}\n"
113
+ f"CRITICAL: The same bot token is being used in MULTIPLE places simultaneously.\n"
114
+ f"Telegram has invalidated the session file for security reasons.\n\n"
115
+ f"✅ Session file has been deleted: {session_file if session_file else 'N/A'}\n\n"
116
+ f"⚠️ TO FIX THIS:\n"
117
+ f" 1. STOP using this bot token in ALL other locations (local servers, other Spaces, etc.)\n"
118
+ f" 2. Wait a few seconds for the other instance to disconnect\n"
119
+ f" 3. Restart this application (the session file will be recreated automatically)\n\n"
120
+ f"📌 REMEMBER: Each bot token can only be used in ONE place at a time!\n"
121
+ f"{'='*60}"
122
+ )
123
+ logger.error(error_msg)
124
+ logger.error(f"Error details: {e}")
125
+
126
+ # Don't re-raise - let the client fail gracefully and app continue in offline mode
127
+ return
128
+ elif type == "string_session":
129
+ # Use string session (user account)
130
+ session_string = session_name_and_token # For string sessions, it's just the string
131
+ client = await Client(
132
+ name=str(client_id),
133
+ api_id=config.API_ID,
134
+ api_hash=config.API_HASH,
135
+ session_string=session_string,
136
+ sleep_threshold=config.SLEEP_THRESHOLD,
137
+ workdir=session_cache_path,
138
+ no_updates=True,
139
+ ).start()
140
+ await client.send_message(
141
+ config.STORAGE_CHANNEL,
142
+ f"Started - String Session Client {client_id}",
143
+ )
144
+ premium_clients[client_id] = client
145
+ premium_work_loads[client_id] = 0
146
+
147
+ logger.info(f"Started - {type.title()} Client {client_id}")
148
+ except Exception as e:
149
+ error_msg = str(e) if str(e) else repr(e)
150
+ error_trace = traceback.format_exc()
151
+ logger.error(
152
+ f"Failed To Start {type.title()} Client - {client_id} Error: {error_msg}"
153
+ )
154
+ logger.error(f"Traceback: {error_trace}")
155
+
156
+ # Start bot clients and string session clients
157
+ await asyncio.gather(
158
+ *(
159
+ [
160
+ start_client(client_id, (session_name, token), "bot")
161
+ for client_id, (session_name, token) in bot_configs.items()
162
+ ]
163
+ + [
164
+ start_client(client_id, session, "string_session")
165
+ for client_id, session in all_string_sessions.items()
166
+ ]
167
+ )
168
+ )
169
+
170
+ clients_initialized = len(multi_clients) > 0
171
+
172
+ if not clients_initialized:
173
+ logger.warning("No Clients Were Initialized - Website will run in offline mode")
174
+ logger.warning("Please configure BOT_TOKENS (and optionally BOT_SESSIONS) in your environment variables")
175
+ return False
176
+
177
+ logger.info("Clients Initialized")
178
+
179
+ # Load the drive data
180
+ await loadDriveData()
181
+
182
+ # Start the backup drive data task
183
+ asyncio.create_task(backup_drive_data())
184
+
185
+ return True
186
+
187
+
188
+ def has_clients():
189
+ """Check if any clients are available"""
190
+ global multi_clients
191
+ return len(multi_clients) > 0
192
+
193
+
194
+ def get_client(premium_required=False) -> Client:
195
+ global multi_clients, work_loads, premium_clients, premium_work_loads
196
+
197
+ if premium_required:
198
+ if not premium_work_loads:
199
+ raise RuntimeError("No Premium Clients Available")
200
+ index = min(premium_work_loads, key=premium_work_loads.get)
201
+ premium_work_loads[index] += 1
202
+ return premium_clients[index]
203
+
204
+ if not work_loads:
205
+ raise RuntimeError("No Clients Available - Check BOT_TOKENS/BOT_SESSIONS configuration")
206
+ index = min(work_loads, key=work_loads.get)
207
+ work_loads[index] += 1
208
+ return multi_clients[index]
utils/directoryHandler.py ADDED
@@ -0,0 +1,409 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import sys
3
+ import config, dill
4
+ from pyrogram.types import InputMediaDocument, Message
5
+ import os, random, string, asyncio
6
+ from utils.logger import Logger
7
+ from datetime import datetime, timezone
8
+ import os
9
+ import signal
10
+
11
+ logger = Logger(__name__)
12
+
13
+ cache_dir = Path("./cache")
14
+ cache_dir.mkdir(parents=True, exist_ok=True)
15
+ drive_cache_path = cache_dir / "drive.data"
16
+
17
+
18
+ def getRandomID():
19
+ global DRIVE_DATA
20
+ while True:
21
+ id = "".join(random.choices(string.ascii_uppercase + string.digits, k=6))
22
+ if not DRIVE_DATA:
23
+ return id
24
+ if id not in DRIVE_DATA.used_ids:
25
+ DRIVE_DATA.used_ids.append(id)
26
+ return id
27
+
28
+
29
+ def get_current_utc_time():
30
+ return datetime.now(timezone.utc).strftime("Date - %Y-%m-%d | Time - %H:%M:%S")
31
+
32
+
33
+ class Folder:
34
+ def __init__(self, name: str, path: str) -> None:
35
+ self.name = name
36
+ self.contents = {}
37
+ if name == "/":
38
+ self.id = "root"
39
+ else:
40
+ self.id = getRandomID()
41
+ self.type = "folder"
42
+ self.trash = False
43
+ self.path = ("/" + path.strip("/") + "/").replace("//", "/")
44
+ self.upload_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
45
+ self.auth_hashes = []
46
+
47
+
48
+ class File:
49
+ def __init__(
50
+ self,
51
+ name: str,
52
+ file_id: int,
53
+ size: int,
54
+ path: str,
55
+ ) -> None:
56
+ self.name = name
57
+ self.file_id = file_id
58
+ self.id = getRandomID()
59
+ self.size = size
60
+ self.type = "file"
61
+ self.trash = False
62
+ self.path = path[:-1] if path[-1] == "/" else path
63
+ self.upload_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
64
+
65
+
66
+ class NewDriveData:
67
+ def __init__(self, contents: dict, used_ids: list) -> None:
68
+ self.contents = contents
69
+ self.used_ids = used_ids
70
+ self.isUpdated = False
71
+
72
+ def save(self) -> None:
73
+ with open(drive_cache_path, "wb") as f:
74
+ dill.dump(self, f)
75
+ self.isUpdated = True
76
+ logger.info("Drive data saved successfully.")
77
+
78
+ def new_folder(self, path: str, name: str) -> None:
79
+ logger.info(f"Creating new folder '{name}' in path '{path}'.")
80
+
81
+ folder = Folder(name, path)
82
+ if path == "/":
83
+ directory_folder: Folder = self.contents[path]
84
+ directory_folder.contents[folder.id] = folder
85
+ else:
86
+ paths = path.strip("/").split("/")
87
+ directory_folder: Folder = self.contents["/"]
88
+ for path in paths:
89
+ directory_folder = directory_folder.contents[path]
90
+ directory_folder.contents[folder.id] = folder
91
+
92
+ self.save()
93
+ return folder.path + folder.id
94
+
95
+ def new_file(self, path: str, name: str, file_id: int, size: int) -> None:
96
+ logger.info(f"Creating new file '{name}' in path '{path}'.")
97
+
98
+ file = File(name, file_id, size, path)
99
+ if path == "/":
100
+ directory_folder: Folder = self.contents[path]
101
+ directory_folder.contents[file.id] = file
102
+ else:
103
+ paths = path.strip("/").split("/")
104
+ directory_folder: Folder = self.contents["/"]
105
+ for path in paths:
106
+ directory_folder = directory_folder.contents[path]
107
+ directory_folder.contents[file.id] = file
108
+
109
+ self.save()
110
+
111
+ def get_directory(
112
+ self, path: str, is_admin: bool = True, auth: str = None
113
+ ) -> Folder:
114
+ folder_data: Folder = self.contents["/"]
115
+ auth_success = False
116
+ auth_home_path = None
117
+
118
+ if path != "/":
119
+ path = path.strip("/")
120
+
121
+ if "/" in path:
122
+ path = path.split("/")
123
+ else:
124
+ path = [path]
125
+
126
+ for folder in path:
127
+ folder_data = folder_data.contents[folder]
128
+
129
+ if auth in folder_data.auth_hashes:
130
+ auth_success = True
131
+ auth_home_path = (
132
+ "/" + folder_data.path.strip("/") + "/" + folder_data.id
133
+ )
134
+
135
+ if not is_admin and not auth_success:
136
+ logger.warning(f"Unauthorized access attempt to path '{path}'.")
137
+ return None
138
+
139
+ if auth_success:
140
+ logger.info(f"Authorization successful for path '{path}'.")
141
+ return folder_data, auth_home_path
142
+
143
+ return folder_data
144
+
145
+ def get_folder_auth(self, path: str) -> None:
146
+ auth = getRandomID()
147
+ folder_data: Folder = self.contents["/"]
148
+
149
+ if path != "/":
150
+ path = path.strip("/")
151
+
152
+ if "/" in path:
153
+ path = path.split("/")
154
+ else:
155
+ path = [path]
156
+
157
+ for folder in path:
158
+ folder_data = folder_data.contents[folder]
159
+
160
+ folder_data.auth_hashes.append(auth)
161
+ self.save()
162
+ logger.info(f"Authorization hash generated for path '{path}'.")
163
+ return auth
164
+
165
+ def get_file(self, path) -> File:
166
+ if len(path.strip("/").split("/")) > 0:
167
+ folder_path = "/" + "/".join(path.strip("/").split("/")[:-1])
168
+ file_id = path.strip("/").split("/")[-1]
169
+ else:
170
+ folder_path = "/"
171
+ file_id = path.strip("/")
172
+
173
+ folder_data = self.get_directory(folder_path)
174
+ return folder_data.contents[file_id]
175
+
176
+ def rename_file_folder(self, path: str, new_name: str) -> None:
177
+ if len(path.strip("/").split("/")) > 0:
178
+ folder_path = "/" + "/".join(path.strip("/").split("/")[:-1])
179
+ file_id = path.strip("/").split("/")[-1]
180
+ else:
181
+ folder_path = "/"
182
+ file_id = path.strip("/")
183
+ folder_data = self.get_directory(folder_path)
184
+ folder_data.contents[file_id].name = new_name
185
+ self.save()
186
+ logger.info(f"Item at path '{path}' renamed to '{new_name}'.")
187
+
188
+ def trash_file_folder(self, path: str, trash: bool) -> None:
189
+ action = "Trashing" if trash else "Restoring"
190
+
191
+ if len(path.strip("/").split("/")) > 0:
192
+ folder_path = "/" + "/".join(path.strip("/").split("/")[:-1])
193
+ file_id = path.strip("/").split("/")[-1]
194
+ else:
195
+ folder_path = "/"
196
+ file_id = path.strip("/")
197
+ folder_data = self.get_directory(folder_path)
198
+ folder_data.contents[file_id].trash = trash
199
+ self.save()
200
+ logger.info(f"Item at path '{path}' {action.lower()} successfully.")
201
+
202
+ def get_trashed_files_folders(self):
203
+ root_dir = self.get_directory("/")
204
+ trash_data = {}
205
+
206
+ def traverse_directory(folder):
207
+ for item in folder.contents.values():
208
+ if item.type == "folder":
209
+ if item.trash:
210
+ trash_data[item.id] = item
211
+ else:
212
+ # Recursively traverse the subfolder
213
+ traverse_directory(item)
214
+ elif item.type == "file":
215
+ if item.trash:
216
+ trash_data[item.id] = item
217
+
218
+ traverse_directory(root_dir)
219
+ return trash_data
220
+
221
+ def delete_file_folder(self, path: str) -> None:
222
+
223
+ if len(path.strip("/").split("/")) > 0:
224
+ folder_path = "/" + "/".join(path.strip("/").split("/")[:-1])
225
+ file_id = path.strip("/").split("/")[-1]
226
+ else:
227
+ folder_path = "/"
228
+ file_id = path.strip("/")
229
+
230
+ folder_data = self.get_directory(folder_path)
231
+ del folder_data.contents[file_id]
232
+ self.save()
233
+ logger.info(f"Item at path '{path}' deleted successfully.")
234
+
235
+ def search_file_folder(self, query: str):
236
+ logger.info(f"Searching for items matching query '{query}'.")
237
+
238
+ root_dir = self.get_directory("/")
239
+ search_results = {}
240
+
241
+ def traverse_directory(folder):
242
+ for item in folder.contents.values():
243
+ if query.lower() in item.name.lower():
244
+ search_results[item.id] = item
245
+ if item.type == "folder":
246
+ traverse_directory(item)
247
+
248
+ traverse_directory(root_dir)
249
+ logger.info(f"Search completed. Found {len(search_results)} matching items.")
250
+ return search_results
251
+
252
+
253
+ class NewBotMode:
254
+ def __init__(self, drive_data: NewDriveData) -> None:
255
+ self.drive_data = drive_data
256
+
257
+ # Set the current folder to root directory by default
258
+ self.current_folder = "/"
259
+ self.current_folder_name = "/ (root directory)"
260
+
261
+ def set_folder(self, folder_path: str, name: str) -> None:
262
+ self.current_folder = folder_path
263
+ self.current_folder_name = name
264
+ self.drive_data.save()
265
+ logger.info(f"Current folder set to '{name}' at path '{folder_path}'.")
266
+
267
+
268
+ DRIVE_DATA: NewDriveData = None
269
+ BOT_MODE: NewBotMode = None
270
+
271
+
272
+ # Function to backup the drive data to telegram
273
+ async def backup_drive_data(loop=True):
274
+ global DRIVE_DATA
275
+ logger.info("Starting backup drive data task.")
276
+
277
+ while True:
278
+ try:
279
+ if not DRIVE_DATA.isUpdated:
280
+ if not loop:
281
+ break
282
+ await asyncio.sleep(config.DATABASE_BACKUP_TIME)
283
+ continue
284
+
285
+ logger.info("Backing up drive data to Telegram.")
286
+ from utils.clients import get_client
287
+
288
+ client = get_client()
289
+ time_text = f"📅 **Last Updated :** {get_current_utc_time()} (UTC +00:00)"
290
+ caption = (
291
+ f"🔐 **TG Drive Data Backup File**\n\n"
292
+ "Do not edit or delete this message. This is a backup file for the tg drive data.\n\n"
293
+ f"{time_text}"
294
+ )
295
+
296
+ media_doc = InputMediaDocument(drive_cache_path, caption=caption)
297
+ msg = await client.edit_message_media(
298
+ config.STORAGE_CHANNEL,
299
+ config.DATABASE_BACKUP_MSG_ID,
300
+ media=media_doc,
301
+ file_name="drive.data",
302
+ )
303
+
304
+ DRIVE_DATA.isUpdated = False
305
+ logger.info("Drive data backed up to Telegram successfully.")
306
+
307
+ try:
308
+ await msg.pin()
309
+ except Exception as pin_e:
310
+ logger.error(f"Error pinning backup message: {pin_e}")
311
+
312
+ if not loop:
313
+ break
314
+
315
+ await asyncio.sleep(config.DATABASE_BACKUP_TIME)
316
+ except Exception as e:
317
+ logger.error(f"Backup Error: {e}")
318
+ await asyncio.sleep(10)
319
+
320
+
321
+ async def init_drive_data():
322
+ global DRIVE_DATA
323
+
324
+ logger.info("Initializing drive data.")
325
+ root_dir = DRIVE_DATA.get_directory("/")
326
+ if not hasattr(root_dir, "auth_hashes"):
327
+ root_dir.auth_hashes = []
328
+
329
+ def traverse_directory(folder):
330
+ for item in folder.contents.values():
331
+ if item.type == "folder":
332
+ traverse_directory(item)
333
+
334
+ if not hasattr(item, "auth_hashes"):
335
+ item.auth_hashes = []
336
+
337
+ traverse_directory(root_dir)
338
+ DRIVE_DATA.save()
339
+ logger.info("Drive data initialization completed.")
340
+
341
+
342
+ async def loadDriveData():
343
+ global DRIVE_DATA, BOT_MODE
344
+
345
+ logger.info("Loading drive data.")
346
+ from utils.clients import get_client
347
+
348
+ try:
349
+ client = get_client()
350
+ try:
351
+ msg: Message = await client.get_messages(
352
+ config.STORAGE_CHANNEL, config.DATABASE_BACKUP_MSG_ID
353
+ )
354
+ except Exception as e:
355
+ logger.error(f"Error fetching backup message: {e}")
356
+ raise
357
+
358
+ if not msg.document:
359
+ logger.error("Backup message does not contain a document")
360
+ raise Exception("Backup message does not contain a document")
361
+
362
+ if msg.document.file_name == "drive.data":
363
+ dl_path = await msg.download()
364
+ with open(dl_path, "rb") as f:
365
+ DRIVE_DATA = dill.load(f)
366
+
367
+ logger.info("Drive data loaded from Telegram backup.")
368
+ else:
369
+ raise Exception("Backup drive.data file not found on Telegram.")
370
+ except Exception as e:
371
+ logger.warning(f"Backup load failed: {e}")
372
+ logger.info("Creating new drive.data file.")
373
+ DRIVE_DATA = NewDriveData({"/": Folder("/", "/")}, [])
374
+ DRIVE_DATA.save()
375
+
376
+ await init_drive_data()
377
+
378
+ if config.MAIN_BOT_TOKEN:
379
+ from utils.bot_mode import start_bot_mode
380
+
381
+ BOT_MODE = NewBotMode(DRIVE_DATA)
382
+ await start_bot_mode(DRIVE_DATA, BOT_MODE)
383
+ logger.info("Bot mode started.")
384
+
385
+
386
+ async def initDriveDataWithoutClients():
387
+ """Initialize DRIVE_DATA without requiring Telegram clients (offline mode)"""
388
+ global DRIVE_DATA, BOT_MODE
389
+
390
+ logger.info("Initializing drive data in offline mode (without Telegram clients).")
391
+
392
+ # Try to load from local cache if it exists
393
+ if drive_cache_path.exists():
394
+ try:
395
+ with open(drive_cache_path, "rb") as f:
396
+ DRIVE_DATA = dill.load(f)
397
+ logger.info("Drive data loaded from local cache.")
398
+ except Exception as e:
399
+ logger.warning(f"Failed to load from local cache: {e}")
400
+ logger.info("Creating new drive.data file.")
401
+ DRIVE_DATA = NewDriveData({"/": Folder("/", "/")}, [])
402
+ DRIVE_DATA.save()
403
+ else:
404
+ logger.info("No local cache found. Creating new drive.data file.")
405
+ DRIVE_DATA = NewDriveData({"/": Folder("/", "/")}, [])
406
+ DRIVE_DATA.save()
407
+
408
+ await init_drive_data()
409
+ logger.info("Drive data initialized in offline mode.")
utils/downloader.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import aiohttp, asyncio
3
+ from utils.extra import get_filename
4
+ from utils.logger import Logger
5
+ from pathlib import Path
6
+ from utils.uploader import start_file_uploader
7
+ from techzdl import TechZDL
8
+
9
+ logger = Logger(__name__)
10
+
11
+ DOWNLOAD_PROGRESS = {}
12
+ STOP_DOWNLOAD = []
13
+
14
+ cache_dir = Path("./cache")
15
+ cache_dir.mkdir(parents=True, exist_ok=True)
16
+
17
+
18
+ async def download_progress_callback(status, current, total, id):
19
+ global DOWNLOAD_PROGRESS
20
+
21
+ DOWNLOAD_PROGRESS[id] = (
22
+ status,
23
+ current,
24
+ total,
25
+ )
26
+
27
+
28
+ async def download_file(url, id, path, filename, singleThreaded):
29
+ global DOWNLOAD_PROGRESS, STOP_DOWNLOAD
30
+
31
+ logger.info(f"Downloading file from {url}")
32
+
33
+ try:
34
+ downloader = TechZDL(
35
+ url,
36
+ output_dir=cache_dir,
37
+ debug=False,
38
+ progress_callback=download_progress_callback,
39
+ progress_args=(id,),
40
+ max_retries=5,
41
+ single_threaded=singleThreaded,
42
+ )
43
+ await downloader.start(in_background=True)
44
+
45
+ await asyncio.sleep(5)
46
+
47
+ while downloader.is_running:
48
+ if id in STOP_DOWNLOAD:
49
+ logger.info(f"Stopping download {id}")
50
+ await downloader.stop()
51
+ return
52
+ await asyncio.sleep(1)
53
+
54
+ if downloader.download_success is False:
55
+ raise downloader.download_error
56
+
57
+ DOWNLOAD_PROGRESS[id] = (
58
+ "completed",
59
+ downloader.total_size,
60
+ downloader.total_size,
61
+ )
62
+
63
+ logger.info(f"File downloaded to {downloader.output_path}")
64
+
65
+ asyncio.create_task(
66
+ start_file_uploader(
67
+ downloader.output_path, id, path, filename, downloader.total_size
68
+ )
69
+ )
70
+ except Exception as e:
71
+ DOWNLOAD_PROGRESS[id] = ("error", 0, 0)
72
+ logger.error(f"Failed to download file: {url} {e}")
73
+
74
+
75
+ async def get_file_info_from_url(url):
76
+ downloader = TechZDL(
77
+ url,
78
+ output_dir=cache_dir,
79
+ debug=False,
80
+ progress_callback=download_progress_callback,
81
+ progress_args=(id,),
82
+ max_retries=5,
83
+ )
84
+ file_info = await downloader.get_file_info()
85
+ return {"file_size": file_info["total_size"], "file_name": file_info["filename"]}
utils/extra.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mimetypes
2
+ from urllib.parse import unquote_plus
3
+ import re
4
+ import urllib.parse
5
+ from pathlib import Path
6
+ from config import WEBSITE_URL
7
+ import asyncio, aiohttp
8
+ from utils.directoryHandler import get_current_utc_time, getRandomID
9
+ from utils.logger import Logger
10
+
11
+ logger = Logger(__name__)
12
+
13
+
14
+ def convert_class_to_dict(data, isObject, showtrash=False):
15
+ if isObject == True:
16
+ data = data.__dict__.copy()
17
+ new_data = {"contents": {}}
18
+
19
+ for key in data["contents"]:
20
+ if data["contents"][key].trash == showtrash:
21
+ if data["contents"][key].type == "folder":
22
+ folder = data["contents"][key]
23
+ new_data["contents"][key] = {
24
+ "name": folder.name,
25
+ "type": folder.type,
26
+ "id": folder.id,
27
+ "path": folder.path,
28
+ "upload_date": folder.upload_date,
29
+ }
30
+ else:
31
+ file = data["contents"][key]
32
+ new_data["contents"][key] = {
33
+ "name": file.name,
34
+ "type": file.type,
35
+ "size": file.size,
36
+ "id": file.id,
37
+ "path": file.path,
38
+ "upload_date": file.upload_date,
39
+ }
40
+ return new_data
41
+
42
+
43
+ async def auto_ping_website():
44
+ if WEBSITE_URL is not None:
45
+ async with aiohttp.ClientSession() as session:
46
+ while True:
47
+ try:
48
+ async with session.get(WEBSITE_URL) as response:
49
+ if response.status == 200:
50
+ logger.info(f"Pinged website at {get_current_utc_time()}")
51
+ else:
52
+ logger.warning(f"Failed to ping website: {response.status}")
53
+ except Exception as e:
54
+ logger.warning(f"Failed to ping website: {e}")
55
+
56
+ await asyncio.sleep(60) # Ping website every minute
57
+
58
+
59
+ import shutil
60
+
61
+
62
+ def reset_cache_dir():
63
+ cache_dir = Path("./cache")
64
+ downloads_dir = Path("./downloads")
65
+
66
+ # Save session files before deleting cache (but skip invalidated ones)
67
+ session_files = {}
68
+ if cache_dir.exists():
69
+ for session_file in cache_dir.glob("*.session"):
70
+ # Check if this session file was marked as invalidated
71
+ invalidated_marker = cache_dir / f"{session_file.name}.invalidated"
72
+ if invalidated_marker.exists():
73
+ logger.warning(f"Skipping invalidated session file: {session_file.name} (was marked as AUTH_KEY_DUPLICATED)")
74
+ # Delete the marker file so it can be retried on next restart
75
+ invalidated_marker.unlink(missing_ok=True)
76
+ continue
77
+ session_files[session_file.name] = session_file.read_bytes()
78
+ logger.info(f"Preserving session file: {session_file.name}")
79
+
80
+ # Delete cache and downloads directories
81
+ shutil.rmtree(cache_dir, ignore_errors=True)
82
+ shutil.rmtree(downloads_dir, ignore_errors=True)
83
+ cache_dir.mkdir(parents=True, exist_ok=True)
84
+ downloads_dir.mkdir(parents=True, exist_ok=True)
85
+
86
+ # Restore session files
87
+ for session_name, session_data in session_files.items():
88
+ (cache_dir / session_name).write_bytes(session_data)
89
+ logger.info(f"Restored session file: {session_name}")
90
+
91
+ logger.info("Cache and downloads directory reset (session files preserved)")
92
+
93
+
94
+ def parse_content_disposition(content_disposition):
95
+ # Split the content disposition into parts
96
+ parts = content_disposition.split(";")
97
+
98
+ # Initialize filename variable
99
+ filename = None
100
+
101
+ # Loop through parts to find the filename
102
+ for part in parts:
103
+ part = part.strip()
104
+ if part.startswith("filename="):
105
+ # If filename is found
106
+ filename = part.split("=", 1)[1]
107
+ elif part.startswith("filename*="):
108
+ # If filename* is found
109
+ match = re.match(r"filename\*=(\S*)''(.*)", part)
110
+ if match:
111
+ encoding, value = match.groups()
112
+ try:
113
+ filename = urllib.parse.unquote(value, encoding=encoding)
114
+ except ValueError:
115
+ # Handle invalid encoding
116
+ pass
117
+
118
+ if filename is None:
119
+ raise Exception("Failed to get filename")
120
+ return filename
121
+
122
+
123
+ def get_filename(headers, url):
124
+ try:
125
+ if headers.get("Content-Disposition"):
126
+ filename = parse_content_disposition(headers["Content-Disposition"])
127
+ else:
128
+ filename = unquote_plus(url.strip("/").split("/")[-1])
129
+
130
+ filename = filename.strip(' "')
131
+ except:
132
+ filename = unquote_plus(url.strip("/").split("/")[-1])
133
+
134
+ filename = filename.strip()
135
+
136
+ if filename == "" or "." not in filename:
137
+ if headers.get("Content-Type"):
138
+ extension = mimetypes.guess_extension(headers["Content-Type"])
139
+ if extension:
140
+ filename = f"{getRandomID()}{extension}"
141
+ else:
142
+ filename = getRandomID()
143
+ else:
144
+ filename = getRandomID()
145
+
146
+ return filename
utils/logger.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from tqdm import tqdm
3
+
4
+
5
+ class TqdmLoggingHandler(logging.Handler):
6
+ def emit(self, record):
7
+ try:
8
+ msg = self.format(record)
9
+ tqdm.write(msg)
10
+ self.flush()
11
+ except Exception:
12
+ self.handleError(record)
13
+
14
+
15
+ class Logger:
16
+ def __init__(self, name, level=logging.DEBUG):
17
+ self.logger = logging.getLogger(name)
18
+ self.logger.setLevel(level)
19
+ self.formatter = logging.Formatter("%(name)s - %(levelname)s - %(message)s")
20
+
21
+ # Remove existing handlers to prevent duplicate logs
22
+ if self.logger.hasHandlers():
23
+ self.logger.handlers.clear()
24
+
25
+ # FileHandler for logging to a file
26
+ file_handler = logging.FileHandler("logs.txt", mode="w")
27
+ file_handler.setFormatter(self.formatter)
28
+ self.logger.addHandler(file_handler)
29
+
30
+ # Custom TqdmLoggingHandler for console output
31
+ stream_handler = TqdmLoggingHandler()
32
+ stream_handler.setFormatter(self.formatter)
33
+ self.logger.addHandler(stream_handler)
34
+
35
+ def debug(self, message):
36
+ self.logger.debug(message)
37
+
38
+ def info(self, message):
39
+ self.logger.info(message)
40
+
41
+ def warning(self, message):
42
+ self.logger.warning(message)
43
+
44
+ def error(self, message):
45
+ self.logger.error(message)
46
+
47
+ def critical(self, message):
48
+ self.logger.critical(message)
utils/mongo_indexer.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from typing import Any, Dict, Optional, Union, Callable, Awaitable
3
+
4
+ from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorCollection
5
+
6
+ from config import MONGODB_URI, MONGODB_DB_NAME, MONGODB_COLLECTION
7
+ from utils.logger import Logger
8
+
9
+ logger = Logger(__name__)
10
+
11
+
12
+ class MongoNotConfiguredError(Exception):
13
+ """Raised when MongoDB is not configured but a Mongo-dependent feature is used."""
14
+
15
+
16
+ _mongo_client: Optional[AsyncIOMotorClient] = None
17
+ _collection: Optional[AsyncIOMotorCollection] = None
18
+
19
+
20
+ def get_mongo_collection() -> AsyncIOMotorCollection:
21
+ """
22
+ Lazily initialize and return the MongoDB collection used to store indexed messages.
23
+ """
24
+ global _mongo_client, _collection
25
+
26
+ if not MONGODB_URI:
27
+ raise MongoNotConfiguredError(
28
+ "MongoDB is not configured. Please set MONGODB_URI in environment."
29
+ )
30
+
31
+ if _mongo_client is None:
32
+ _mongo_client = AsyncIOMotorClient(MONGODB_URI)
33
+
34
+ if _collection is None:
35
+ db = _mongo_client[MONGODB_DB_NAME]
36
+ _collection = db[MONGODB_COLLECTION]
37
+
38
+ return _collection
39
+
40
+
41
+ async def _ensure_indexes(col: AsyncIOMotorCollection) -> None:
42
+ """Ensure indexes required for fast lookups and deduplication."""
43
+ try:
44
+ await col.create_index(
45
+ [("chat_id", 1), ("message_id", 1)],
46
+ name="chat_message_unique",
47
+ unique=True,
48
+ )
49
+ await col.create_index(
50
+ [("chat_id", 1), ("date", -1)],
51
+ name="chat_date_desc",
52
+ )
53
+ except Exception as e:
54
+ logger.error(f"Error creating MongoDB indexes: {e}")
55
+
56
+
57
+ StatusCallback = Optional[Callable[[str], Awaitable[None]]]
58
+
59
+
60
+ async def index_channel_messages(
61
+ client,
62
+ channel: Union[int, str],
63
+ status_callback: StatusCallback = None,
64
+ ) -> int:
65
+ """
66
+ Index all messages of a given channel/chat into MongoDB.
67
+
68
+ Returns the number of messages processed.
69
+ """
70
+ col = get_mongo_collection()
71
+ await _ensure_indexes(col)
72
+
73
+ total = 0
74
+ last_report = 0
75
+
76
+ async def _report(force: bool = False) -> None:
77
+ nonlocal last_report, total
78
+ if not status_callback:
79
+ return
80
+ if force or total - last_report >= 100:
81
+ last_report = total
82
+ try:
83
+ await status_callback(f"Indexed {total} messages so far...")
84
+ except Exception as e:
85
+ logger.error(f"Status callback error: {e}")
86
+
87
+ logger.info(f"Starting indexing for channel: {channel}")
88
+
89
+ try:
90
+ async for msg in client.get_chat_history(channel, limit=0):
91
+ try:
92
+ data: Dict[str, Any] = msg.to_dict()
93
+ doc = {
94
+ "chat_id": msg.chat.id if msg.chat else None,
95
+ "message_id": msg.id,
96
+ "date": msg.date,
97
+ "from_user_id": msg.from_user.id if msg.from_user else None,
98
+ "text": msg.text or msg.caption,
99
+ "raw": data,
100
+ }
101
+
102
+ await col.update_one(
103
+ {"chat_id": doc["chat_id"], "message_id": doc["message_id"]},
104
+ {"$set": doc},
105
+ upsert=True,
106
+ )
107
+
108
+ total += 1
109
+ if total % 100 == 0:
110
+ await _report()
111
+ except Exception as inner_e:
112
+ logger.error(f"Error indexing message {getattr(msg, 'id', 'unknown')}: {inner_e}")
113
+ continue
114
+
115
+ await _report(force=True)
116
+ logger.info(f"Completed indexing for channel: {channel} | Total messages: {total}")
117
+ return total
118
+ except Exception as e:
119
+ logger.error(f"Failed to index channel {channel}: {e}")
120
+ raise
121
+
utils/movie_requests.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from typing import Optional, Dict, Any
3
+
4
+ from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorCollection
5
+
6
+ from config import (
7
+ MONGODB_URI,
8
+ MONGODB_DB_NAME,
9
+ MONGODB_COLLECTION,
10
+ MONGODB_REQUESTS_COLLECTION,
11
+ )
12
+ from utils.logger import Logger
13
+
14
+ logger = Logger(__name__)
15
+
16
+ _client: Optional[AsyncIOMotorClient] = None
17
+ _messages_col: Optional[AsyncIOMotorCollection] = None
18
+ _requests_col: Optional[AsyncIOMotorCollection] = None
19
+
20
+
21
+ class MovieRequestsMongoNotConfigured(Exception):
22
+ """Raised when MongoDB is not configured for movie requests."""
23
+
24
+
25
+ def _get_client() -> AsyncIOMotorClient:
26
+ global _client
27
+ if not MONGODB_URI:
28
+ raise MovieRequestsMongoNotConfigured(
29
+ "MongoDB is not configured. Please set MONGODB_URI in environment."
30
+ )
31
+ if _client is None:
32
+ _client = AsyncIOMotorClient(MONGODB_URI)
33
+ return _client
34
+
35
+
36
+ def get_messages_collection() -> AsyncIOMotorCollection:
37
+ """Mongo collection where indexed Telegram messages are stored."""
38
+ global _messages_col
39
+ if _messages_col is None:
40
+ client = _get_client()
41
+ db = client[MONGODB_DB_NAME]
42
+ _messages_col = db[MONGODB_COLLECTION]
43
+ return _messages_col
44
+
45
+
46
+ def get_requests_collection() -> AsyncIOMotorCollection:
47
+ """Mongo collection where movie requests are stored."""
48
+ global _requests_col
49
+ if _requests_col is None:
50
+ client = _get_client()
51
+ db = client[MONGODB_DB_NAME]
52
+ _requests_col = db[MONGODB_REQUESTS_COLLECTION]
53
+ return _requests_col
54
+
55
+
56
+ async def save_movie_request(
57
+ title: str,
58
+ year: str,
59
+ available: bool,
60
+ file_info: Optional[Dict[str, Any]] = None,
61
+ ) -> None:
62
+ col = get_requests_collection()
63
+ doc: Dict[str, Any] = {
64
+ "title": title,
65
+ "year": str(year),
66
+ "available": available,
67
+ "created_at": datetime.utcnow(),
68
+ }
69
+ if file_info:
70
+ doc["file"] = file_info
71
+ try:
72
+ await col.insert_one(doc)
73
+ except Exception as e:
74
+ logger.error(f"Failed to save movie request in MongoDB: {e}")
75
+
utils/streamer/__init__.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math, mimetypes
2
+ from fastapi.responses import StreamingResponse, Response
3
+ from utils.logger import Logger
4
+ from utils.streamer.custom_dl import ByteStreamer
5
+ from utils.streamer.file_properties import get_name
6
+ from utils.clients import (
7
+ get_client,
8
+ )
9
+ from urllib.parse import quote
10
+
11
+ logger = Logger(__name__)
12
+
13
+ class_cache = {}
14
+
15
+
16
+ async def media_streamer(channel: int, message_id: int, file_name: str, request):
17
+ global class_cache
18
+
19
+ range_header = request.headers.get("Range", 0)
20
+
21
+ faster_client = get_client()
22
+
23
+ if faster_client in class_cache:
24
+ tg_connect = class_cache[faster_client]
25
+ else:
26
+ tg_connect = ByteStreamer(faster_client)
27
+ class_cache[faster_client] = tg_connect
28
+
29
+ file_id = await tg_connect.get_file_properties(channel, message_id)
30
+ file_size = file_id.file_size
31
+
32
+ if range_header:
33
+ from_bytes, until_bytes = range_header.replace("bytes=", "").split("-")
34
+ from_bytes = int(from_bytes)
35
+ until_bytes = int(until_bytes) if until_bytes else file_size - 1
36
+ else:
37
+ from_bytes = 0
38
+ until_bytes = file_size - 1
39
+
40
+ if (until_bytes > file_size) or (from_bytes < 0) or (until_bytes < from_bytes):
41
+ return Response(
42
+ status_code=416,
43
+ content="416: Range not satisfiable",
44
+ headers={"Content-Range": f"bytes */{file_size}"},
45
+ )
46
+
47
+ chunk_size = 1024 * 1024
48
+ until_bytes = min(until_bytes, file_size - 1)
49
+
50
+ offset = from_bytes - (from_bytes % chunk_size)
51
+ first_part_cut = from_bytes - offset
52
+ last_part_cut = until_bytes % chunk_size + 1
53
+
54
+ req_length = until_bytes - from_bytes + 1
55
+ part_count = math.ceil(until_bytes / chunk_size) - math.floor(offset / chunk_size)
56
+ body = tg_connect.yield_file(
57
+ file_id, offset, first_part_cut, last_part_cut, part_count, chunk_size
58
+ )
59
+
60
+ disposition = "attachment"
61
+ mime_type = mimetypes.guess_type(file_name.lower())[0] or "application/octet-stream"
62
+
63
+ if (
64
+ "video/" in mime_type
65
+ or "audio/" in mime_type
66
+ or "image/" in mime_type
67
+ or "/html" in mime_type
68
+ ):
69
+ disposition = "inline"
70
+
71
+ return StreamingResponse(
72
+ status_code=206 if range_header else 200,
73
+ content=body,
74
+ headers={
75
+ "Content-Type": f"{mime_type}",
76
+ "Content-Range": f"bytes {from_bytes}-{until_bytes}/{file_size}",
77
+ "Content-Length": str(req_length),
78
+ "Content-Disposition": f'{disposition}; filename="{quote(file_name)}"',
79
+ "Accept-Ranges": "bytes",
80
+ },
81
+ media_type=mime_type,
82
+ )
utils/streamer/custom_dl.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from typing import Dict, Union
3
+ from pyrogram import Client, utils, raw
4
+ from .file_properties import get_file_ids
5
+ from pyrogram.session import Session, Auth
6
+ from pyrogram.errors import AuthBytesInvalid
7
+ from pyrogram.file_id import FileId, FileType, ThumbnailSource
8
+ from utils.logger import Logger
9
+
10
+ logger = Logger(__name__)
11
+
12
+
13
+ class ByteStreamer:
14
+ def __init__(self, client: Client):
15
+ self.clean_timer = 30 * 60
16
+ self.client: Client = client
17
+ self.cached_file_ids: Dict[int, FileId] = {}
18
+ self.dc_options = None
19
+ asyncio.create_task(self.clean_cache())
20
+
21
+ async def get_file_properties(self, channel, message_id: int) -> FileId:
22
+ if message_id not in self.cached_file_ids:
23
+ await self.generate_file_properties(channel, message_id)
24
+ return self.cached_file_ids[message_id]
25
+
26
+ async def generate_file_properties(self, channel, message_id: int) -> FileId:
27
+ file_id = await get_file_ids(self.client, channel, message_id)
28
+ if not file_id:
29
+ raise Exception("FileNotFound")
30
+ self.cached_file_ids[message_id] = file_id
31
+ return self.cached_file_ids[message_id]
32
+
33
+ async def generate_media_session(self, client: Client, file_id: FileId) -> Session:
34
+ """
35
+ Generates the media session for the DC that contains the media file.
36
+ This is required for getting the bytes from Telegram servers.
37
+ """
38
+
39
+ media_session = client.media_sessions.get(file_id.dc_id, None)
40
+
41
+ if media_session is None:
42
+ if not self.dc_options:
43
+ config = await client.invoke(raw.functions.help.GetConfig())
44
+ self.dc_options = config.dc_options
45
+
46
+ ip = None
47
+ port = None
48
+ for option in self.dc_options:
49
+ if option.id == file_id.dc_id and not option.ipv6 and not option.cdn and not option.media_only:
50
+ ip = option.ip_address
51
+ port = option.port
52
+ break
53
+
54
+ if not ip:
55
+ for option in self.dc_options:
56
+ if option.id == file_id.dc_id and not option.ipv6 and not option.cdn:
57
+ ip = option.ip_address
58
+ port = option.port
59
+ break
60
+
61
+ if file_id.dc_id != await client.storage.dc_id():
62
+ media_session = Session(
63
+ client,
64
+ file_id.dc_id,
65
+ ip,
66
+ port,
67
+ await Auth(
68
+ client, file_id.dc_id, ip, port, await client.storage.test_mode()
69
+ ).create(),
70
+ await client.storage.test_mode(),
71
+ is_media=True,
72
+ )
73
+ await media_session.start()
74
+
75
+ for _ in range(6):
76
+ exported_auth = await client.invoke(
77
+ raw.functions.auth.ExportAuthorization(dc_id=file_id.dc_id)
78
+ )
79
+
80
+ try:
81
+ await media_session.invoke(
82
+ raw.functions.auth.ImportAuthorization(
83
+ id=exported_auth.id, bytes=exported_auth.bytes
84
+ )
85
+ )
86
+ break
87
+ except AuthBytesInvalid:
88
+ logger.debug(
89
+ f"Invalid authorization bytes for DC {file_id.dc_id}"
90
+ )
91
+ continue
92
+ else:
93
+ await media_session.stop()
94
+ raise AuthBytesInvalid
95
+ else:
96
+ media_session = Session(
97
+ client,
98
+ file_id.dc_id,
99
+ ip,
100
+ port,
101
+ await client.storage.auth_key(),
102
+ await client.storage.test_mode(),
103
+ is_media=True,
104
+ )
105
+ await media_session.start()
106
+ logger.debug(f"Created media session for DC {file_id.dc_id}")
107
+ client.media_sessions[file_id.dc_id] = media_session
108
+ else:
109
+ logger.debug(f"Using cached media session for DC {file_id.dc_id}")
110
+ return media_session
111
+
112
+ @staticmethod
113
+ async def get_location(
114
+ file_id: FileId,
115
+ ) -> Union[
116
+ raw.types.InputPhotoFileLocation,
117
+ raw.types.InputDocumentFileLocation,
118
+ raw.types.InputPeerPhotoFileLocation,
119
+ ]:
120
+ """
121
+ Returns the file location for the media file.
122
+ """
123
+ file_type = file_id.file_type
124
+
125
+ if file_type == FileType.CHAT_PHOTO:
126
+ if file_id.chat_id > 0:
127
+ peer = raw.types.InputPeerUser(
128
+ user_id=file_id.chat_id, access_hash=file_id.chat_access_hash
129
+ )
130
+ else:
131
+ if file_id.chat_access_hash == 0:
132
+ peer = raw.types.InputPeerChat(chat_id=-file_id.chat_id)
133
+ else:
134
+ peer = raw.types.InputPeerChannel(
135
+ channel_id=utils.get_channel_id(file_id.chat_id),
136
+ access_hash=file_id.chat_access_hash,
137
+ )
138
+
139
+ location = raw.types.InputPeerPhotoFileLocation(
140
+ peer=peer,
141
+ volume_id=file_id.volume_id,
142
+ local_id=file_id.local_id,
143
+ big=file_id.thumbnail_source == ThumbnailSource.CHAT_PHOTO_BIG,
144
+ )
145
+ elif file_type == FileType.PHOTO:
146
+ location = raw.types.InputPhotoFileLocation(
147
+ id=file_id.media_id,
148
+ access_hash=file_id.access_hash,
149
+ file_reference=file_id.file_reference,
150
+ thumb_size=file_id.thumbnail_size,
151
+ )
152
+ else:
153
+ location = raw.types.InputDocumentFileLocation(
154
+ id=file_id.media_id,
155
+ access_hash=file_id.access_hash,
156
+ file_reference=file_id.file_reference,
157
+ thumb_size=file_id.thumbnail_size,
158
+ )
159
+ return location
160
+
161
+ async def yield_file(
162
+ self,
163
+ file_id: FileId,
164
+ offset: int,
165
+ first_part_cut: int,
166
+ last_part_cut: int,
167
+ part_count: int,
168
+ chunk_size: int,
169
+ ):
170
+ """
171
+ Custom generator that yields the bytes of the media file.
172
+ """
173
+ client = self.client
174
+ logger.debug(f"Starting to yielding file with client.")
175
+ media_session = await self.generate_media_session(client, file_id)
176
+
177
+ current_part = 1
178
+ location = await self.get_location(file_id)
179
+
180
+ try:
181
+ r = await media_session.invoke(
182
+ raw.functions.upload.GetFile(
183
+ location=location, offset=offset, limit=chunk_size
184
+ ),
185
+ )
186
+ if isinstance(r, raw.types.upload.File):
187
+ while True:
188
+ chunk = r.bytes
189
+ if not chunk:
190
+ break
191
+ elif part_count == 1:
192
+ yield chunk[first_part_cut:last_part_cut]
193
+ elif current_part == 1:
194
+ yield chunk[first_part_cut:]
195
+ elif current_part == part_count:
196
+ yield chunk[:last_part_cut]
197
+ else:
198
+ yield chunk
199
+
200
+ current_part += 1
201
+ offset += chunk_size
202
+
203
+ if current_part > part_count:
204
+ break
205
+
206
+ r = await media_session.invoke(
207
+ raw.functions.upload.GetFile(
208
+ location=location, offset=offset, limit=chunk_size
209
+ ),
210
+ )
211
+ except (TimeoutError, AttributeError):
212
+ pass
213
+ finally:
214
+ logger.debug(f"Finished yielding file with {current_part} parts.")
215
+
216
+ async def clean_cache(self) -> None:
217
+ """
218
+ function to clean the cache to reduce memory usage
219
+ """
220
+ while True:
221
+ await asyncio.sleep(self.clean_timer)
222
+ self.cached_file_ids.clear()
223
+ logger.debug("Cleaned the cache")
utils/streamer/file_properties.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pyrogram import Client
2
+ from pyrogram.types import Message
3
+ from pyrogram.file_id import FileId
4
+ from typing import Any, Optional, Union
5
+ from pyrogram.raw.types.messages import Messages
6
+ from datetime import datetime
7
+
8
+
9
+ async def parse_file_id(message: "Message") -> Optional[FileId]:
10
+ media = get_media_from_message(message)
11
+ if media:
12
+ return FileId.decode(media.file_id)
13
+
14
+
15
+ async def parse_file_unique_id(message: "Messages") -> Optional[str]:
16
+ media = get_media_from_message(message)
17
+ if media:
18
+ return media.file_unique_id
19
+
20
+
21
+ async def get_file_ids(client: Client, chat_id, message_id) -> Optional[FileId]:
22
+ message = await client.get_messages(chat_id, int(message_id))
23
+ if message.empty:
24
+ raise Exception("FileNotFound")
25
+ media = get_media_from_message(message)
26
+ file_unique_id = await parse_file_unique_id(message)
27
+ file_id = await parse_file_id(message)
28
+ setattr(file_id, "file_size", getattr(media, "file_size", 0))
29
+ setattr(file_id, "mime_type", getattr(media, "mime_type", ""))
30
+ setattr(file_id, "file_name", getattr(media, "file_name", ""))
31
+ setattr(file_id, "unique_id", file_unique_id)
32
+ return file_id
33
+
34
+
35
+ def get_media_from_message(message: "Message") -> Any:
36
+ media_types = (
37
+ "audio",
38
+ "document",
39
+ "photo",
40
+ "sticker",
41
+ "animation",
42
+ "video",
43
+ "voice",
44
+ "video_note",
45
+ )
46
+ for attr in media_types:
47
+ media = getattr(message, attr, None)
48
+ if media:
49
+ return media
50
+
51
+
52
+ def get_name(media_msg: Union[Message, FileId]) -> str:
53
+ if isinstance(media_msg, Message):
54
+ media = get_media_from_message(media_msg)
55
+ file_name = getattr(media, "file_name", "")
56
+
57
+ elif isinstance(media_msg, FileId):
58
+ file_name = getattr(media_msg, "file_name", "")
59
+
60
+ if not file_name:
61
+ if isinstance(media_msg, Message) and media_msg.media:
62
+ media_type = media_msg.media.value
63
+ elif media_msg.file_type:
64
+ media_type = media_msg.file_type.name.lower()
65
+ else:
66
+ media_type = "file"
67
+
68
+ formats = {
69
+ "photo": "jpg",
70
+ "audio": "mp3",
71
+ "voice": "ogg",
72
+ "video": "mp4",
73
+ "animation": "mp4",
74
+ "video_note": "mp4",
75
+ "sticker": "webp",
76
+ }
77
+
78
+ ext = formats.get(media_type)
79
+ ext = "." + ext if ext else ""
80
+
81
+ date = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
82
+ file_name = f"{media_type}-{date}{ext}"
83
+
84
+ return file_name
utils/uploader.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from utils.clients import get_client
2
+ from pyrogram import Client
3
+ from pyrogram.types import Message
4
+ from pyrogram.errors import AuthKeyDuplicated
5
+ from config import STORAGE_CHANNEL
6
+ import os
7
+ from pathlib import Path
8
+ from utils.logger import Logger
9
+ from urllib.parse import unquote_plus
10
+
11
+ logger = Logger(__name__)
12
+ PROGRESS_CACHE = {}
13
+ STOP_TRANSMISSION = []
14
+
15
+
16
+ async def progress_callback(current, total, id, client: Client, file_path):
17
+ global PROGRESS_CACHE, STOP_TRANSMISSION
18
+
19
+ PROGRESS_CACHE[id] = ("running", current, total)
20
+ if id in STOP_TRANSMISSION:
21
+ logger.info(f"Stopping transmission {id}")
22
+ client.stop_transmission()
23
+ try:
24
+ os.remove(file_path)
25
+ except:
26
+ pass
27
+
28
+
29
+ async def start_file_uploader(
30
+ file_path, id, directory_path, filename, file_size, delete=True
31
+ ):
32
+ global PROGRESS_CACHE
33
+ from utils.directoryHandler import DRIVE_DATA
34
+
35
+ logger.info(f"Uploading file {file_path} {id}")
36
+
37
+ if file_size > 1.98 * 1024 * 1024 * 1024:
38
+ # Use premium client for files larger than 2 GB
39
+ client: Client = get_client(premium_required=True)
40
+ else:
41
+ client: Client = get_client()
42
+
43
+ PROGRESS_CACHE[id] = ("running", 0, 0)
44
+
45
+ try:
46
+ message: Message = await client.send_document(
47
+ STORAGE_CHANNEL,
48
+ file_path,
49
+ progress=progress_callback,
50
+ progress_args=(id, client, file_path),
51
+ disable_notification=True,
52
+ )
53
+ except AuthKeyDuplicated as e:
54
+ error_msg = (
55
+ "AUTH_KEY_DUPLICATED: The same bot token is being used in multiple places simultaneously. "
56
+ "The session file has been invalidated by Telegram. "
57
+ "Please ensure you're not running the same bot token locally and on Hugging Face Spaces at the same time. "
58
+ "Delete the session file and restart the application."
59
+ )
60
+ logger.error(error_msg)
61
+ logger.error(f"Error details: {e}")
62
+
63
+ # Try to delete the session file if possible
64
+ try:
65
+ session_cache_path = Path("./cache")
66
+ session_file = session_cache_path / f"{client.name}.session"
67
+ if session_file.exists():
68
+ session_file.unlink()
69
+ logger.info(f"Deleted invalidated session file: {session_file}")
70
+ except Exception as cleanup_error:
71
+ logger.error(f"Failed to delete session file: {cleanup_error}")
72
+
73
+ PROGRESS_CACHE[id] = ("failed", 0, 0)
74
+ if delete:
75
+ try:
76
+ os.remove(file_path)
77
+ except:
78
+ pass
79
+ return
80
+ size = (
81
+ message.photo
82
+ or message.document
83
+ or message.video
84
+ or message.audio
85
+ or message.sticker
86
+ ).file_size
87
+
88
+ filename = unquote_plus(filename)
89
+
90
+ DRIVE_DATA.new_file(directory_path, filename, message.id, size)
91
+ PROGRESS_CACHE[id] = ("completed", size, size)
92
+
93
+ logger.info(f"Uploaded file {file_path} {id}")
94
+
95
+ if delete:
96
+ try:
97
+ os.remove(file_path)
98
+ except Exception as e:
99
+ pass
website/VideoPlayer.html ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>TG Drive - Video Player</title>
7
+
8
+ <link href="//vjs.zencdn.net/8.3.0/video-js.min.css" rel="stylesheet">
9
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/videojs-seek-buttons/3.0.1/videojs-seek-buttons.min.css" rel="stylesheet">
10
+
11
+ <script src="//vjs.zencdn.net/8.3.0/video.min.js"></script>
12
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/videojs-seek-buttons/3.0.1/videojs-seek-buttons.min.js"></script>
13
+
14
+ <style>
15
+ /* Modern Reset & Base Styles */
16
+ body {
17
+ display: flex;
18
+ flex-direction: column;
19
+ align-items: center;
20
+ justify-content: center;
21
+ min-height: 100vh;
22
+ margin: 0;
23
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
24
+ background-color: #0f172a;
25
+ background-image:
26
+ radial-gradient(at 0% 0%, hsla(253,16%,7%,1) 0, transparent 50%),
27
+ radial-gradient(at 50% 0%, hsla(225,39%,30%,1) 0, transparent 50%),
28
+ radial-gradient(at 100% 0%, hsla(339,49%,30%,1) 0, transparent 50%);
29
+ color: #ffffff;
30
+ user-select: none; /* Prevents text selection on double tap */
31
+ }
32
+
33
+ .glass-panel {
34
+ background: rgba(255, 255, 255, 0.05);
35
+ backdrop-filter: blur(16px);
36
+ -webkit-backdrop-filter: blur(16px);
37
+ border: 1px solid rgba(255, 255, 255, 0.1);
38
+ border-radius: 24px;
39
+ padding: 24px;
40
+ width: 90%;
41
+ max-width: 960px;
42
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
43
+ display: flex;
44
+ flex-direction: column;
45
+ align-items: center;
46
+ gap: 24px;
47
+ }
48
+
49
+ .video-container {
50
+ width: 100%;
51
+ border-radius: 16px;
52
+ overflow: hidden;
53
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
54
+ position: relative; /* For double tap overlay */
55
+ }
56
+
57
+ /* --- Custom Video.js Styling --- */
58
+
59
+ /* Center Play Button */
60
+ .video-js .vjs-big-play-button {
61
+ background-color: rgba(59, 130, 246, 0.9);
62
+ border: none;
63
+ width: 80px;
64
+ height: 80px;
65
+ line-height: 80px;
66
+ border-radius: 50%;
67
+ margin-left: -40px;
68
+ margin-top: -40px;
69
+ transition: all 0.3s ease;
70
+ }
71
+ .video-js .vjs-big-play-button:hover {
72
+ background-color: #2563eb;
73
+ transform: scale(1.1);
74
+ }
75
+
76
+ /* Control Bar Transparency */
77
+ .video-js .vjs-control-bar {
78
+ background-color: rgba(15, 23, 42, 0.85);
79
+ border-radius: 0 0 16px 16px;
80
+ }
81
+
82
+ /* Seek Buttons Styling */
83
+ .video-js .vjs-seek-button {
84
+ font-size: 1.2em;
85
+ cursor: pointer;
86
+ }
87
+
88
+ /* Speed Menu Styling */
89
+ .video-js .vjs-playback-rate .vjs-playback-rate-value {
90
+ line-height: 30px;
91
+ font-weight: bold;
92
+ }
93
+
94
+ /* --- Double Tap Overlay Animation --- */
95
+ .double-tap-overlay {
96
+ position: absolute;
97
+ top: 0;
98
+ bottom: 0;
99
+ width: 40%;
100
+ display: flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ z-index: 10;
104
+ opacity: 0;
105
+ transition: opacity 0.2s;
106
+ pointer-events: none; /* Let clicks pass through if needed, but we handle via JS */
107
+ color: rgba(255,255,255,0.8);
108
+ font-size: 40px;
109
+ }
110
+ .dt-left { left: 0; background: linear-gradient(90deg, rgba(0,0,0,0.3), transparent); }
111
+ .dt-right { right: 0; background: linear-gradient(-90deg, rgba(0,0,0,0.3), transparent); }
112
+
113
+ .dt-icon {
114
+ background: rgba(0,0,0,0.5);
115
+ border-radius: 50%;
116
+ padding: 15px;
117
+ display: none; /* Hidden by default */
118
+ }
119
+
120
+ /* Buttons Area */
121
+ .buttons-container {
122
+ display: flex;
123
+ gap: 16px;
124
+ width: 100%;
125
+ justify-content: center;
126
+ flex-wrap: wrap;
127
+ }
128
+
129
+ .copy-button {
130
+ padding: 14px 28px;
131
+ font-size: 15px;
132
+ font-weight: 600;
133
+ cursor: pointer;
134
+ background: rgba(255, 255, 255, 0.1);
135
+ color: white;
136
+ border: 1px solid rgba(255, 255, 255, 0.2);
137
+ border-radius: 12px;
138
+ transition: all 0.3s ease;
139
+ display: flex;
140
+ align-items: center;
141
+ justify-content: center;
142
+ gap: 8px;
143
+ min-width: 180px;
144
+ }
145
+
146
+ .copy-button:hover {
147
+ background: rgba(255, 255, 255, 0.2);
148
+ transform: translateY(-2px);
149
+ border-color: rgba(255, 255, 255, 0.4);
150
+ }
151
+
152
+ .copy-button.success {
153
+ background-color: #10b981;
154
+ border-color: #10b981;
155
+ }
156
+ </style>
157
+ </head>
158
+
159
+ <body>
160
+
161
+ <div class="glass-panel">
162
+ <div class="video-container" id="video-wrapper">
163
+
164
+ <div class="double-tap-overlay dt-left" id="dt-left">
165
+ <div class="dt-icon">⏪ 10s</div>
166
+ </div>
167
+ <div class="double-tap-overlay dt-right" id="dt-right">
168
+ <div class="dt-icon">10s ⏩</div>
169
+ </div>
170
+
171
+ <video id="my-player" class="video-js vjs-fluid vjs-big-play-centered" controls preload="auto"
172
+ data-setup='{"playbackRates": [0.5, 1, 1.25, 1.5, 2]}'>
173
+ <source id="video-src" src="" type="video/mp4">
174
+ </source>
175
+ <p class="vjs-no-js">
176
+ To view this video please enable JavaScript.
177
+ </p>
178
+ </video>
179
+ </div>
180
+
181
+ <div class="buttons-container">
182
+ <button class="copy-button" onclick="copyStreamUrl(this)">
183
+ Copy Stream URL
184
+ </button>
185
+ <button class="copy-button" onclick="copyDownloadUrl(this)">
186
+ Copy Download URL
187
+ </button>
188
+ </div>
189
+ </div>
190
+
191
+ <script>
192
+ // 1. Get URL and Set Source
193
+ const downloadUrl = (new URL(window.location.href)).searchParams.get('url');
194
+ document.getElementById('video-src').src = downloadUrl;
195
+
196
+ // 2. Initialize Video.js Features
197
+ const player = videojs('my-player');
198
+
199
+ player.ready(function() {
200
+ // Enable the Seek Buttons Plugin (Forward/Back 10s)
201
+ player.seekButtons({
202
+ forward: 10,
203
+ back: 10
204
+ });
205
+ });
206
+
207
+ // 3. Custom Double Tap Logic
208
+ const wrapper = document.getElementById('video-wrapper');
209
+ const dtLeft = document.getElementById('dt-left');
210
+ const dtRight = document.getElementById('dt-right');
211
+ let lastTapTime = 0;
212
+
213
+ // Listen for taps on the wrapper (captures clicks over the video)
214
+ wrapper.addEventListener('click', function(e) {
215
+ const currentTime = new Date().getTime();
216
+ const tapLength = currentTime - lastTapTime;
217
+
218
+ // If double tap (less than 300ms between taps)
219
+ if (tapLength < 300 && tapLength > 0) {
220
+ const rect = wrapper.getBoundingClientRect();
221
+ const x = e.clientX - rect.left; // Click position inside video
222
+
223
+ // Check if click is on Left (0-40%) or Right (60-100%) side
224
+ if (x < rect.width * 0.4) {
225
+ // Rewind
226
+ player.currentTime(player.currentTime() - 10);
227
+ showDoubleTapEffect(dtLeft);
228
+ } else if (x > rect.width * 0.6) {
229
+ // Forward
230
+ player.currentTime(player.currentTime() + 10);
231
+ showDoubleTapEffect(dtRight);
232
+ }
233
+ e.preventDefault(); // Stop default play/pause on the second click
234
+ }
235
+ lastTapTime = currentTime;
236
+ });
237
+
238
+ function showDoubleTapEffect(element) {
239
+ const icon = element.querySelector('.dt-icon');
240
+ element.style.opacity = '1';
241
+ icon.style.display = 'block';
242
+
243
+ setTimeout(() => {
244
+ element.style.opacity = '0';
245
+ setTimeout(() => { icon.style.display = 'none'; }, 200);
246
+ }, 500);
247
+ }
248
+
249
+ // 4. Clipboard Logic (Same as before)
250
+ function copyTextToClipboard(text, btnElement) {
251
+ if (navigator.clipboard && navigator.clipboard.writeText) {
252
+ navigator.clipboard.writeText(text).then(() => animateButton(btnElement))
253
+ .catch(() => fallbackCopyTextToClipboard(text, btnElement));
254
+ } else {
255
+ fallbackCopyTextToClipboard(text, btnElement);
256
+ }
257
+ }
258
+
259
+ function fallbackCopyTextToClipboard(text, btnElement) {
260
+ const textArea = document.createElement('textarea');
261
+ textArea.value = text;
262
+ document.body.appendChild(textArea);
263
+ textArea.select();
264
+ try {
265
+ if (document.execCommand('copy')) animateButton(btnElement);
266
+ else alert('Failed to copy');
267
+ } catch (e) {}
268
+ document.body.removeChild(textArea);
269
+ }
270
+
271
+ function animateButton(btn) {
272
+ const originalText = btn.innerText;
273
+ btn.classList.add('success');
274
+ btn.innerText = "Copied!";
275
+ setTimeout(() => {
276
+ btn.classList.remove('success');
277
+ btn.innerText = originalText;
278
+ }, 2000);
279
+ }
280
+
281
+ function copyStreamUrl(btn) { copyTextToClipboard(window.location.href, btn); }
282
+ function copyDownloadUrl(btn) { copyTextToClipboard(downloadUrl, btn); }
283
+ </script>
284
+
285
+ </body>
286
+ </html>
website/home.html ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>TG Drive</title>
8
+ <link rel="stylesheet" href="static/home.css?v=2" />
9
+
10
+ <!-- Fonts Start -->
11
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
12
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
13
+ <link
14
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Space+Grotesk:wght@400;500;600;700&display=swap"
15
+ rel="stylesheet" />
16
+ <!-- Fonts End -->
17
+ </head>
18
+
19
+ <body>
20
+ <div class="container">
21
+ <!-- Sidebar Start -->
22
+ <div class="sidebar">
23
+ <div class="sidebar-header">
24
+ <img src="https://ssl.gstatic.com/images/branding/product/1x/drive_2020q4_48dp.png" />
25
+ <span>TG Drive</span>
26
+ </div>
27
+
28
+ <button id="new-button" class="new-button">
29
+ <img src="static/assets/plus-icon.svg" />New
30
+ </button>
31
+
32
+ <div id="new-upload" class="new-upload">
33
+ <input id="new-upload-focus" type="text"
34
+ style="height: 0px; width: 0px; border: none; position: absolute" readonly />
35
+ <div id="new-folder-btn">
36
+ <img src="static/assets/folder-icon.svg" />
37
+ New Folder
38
+ </div>
39
+ <hr />
40
+ <div id="file-upload-btn">
41
+ <img src="static/assets/upload-icon.svg" />
42
+ File Upload
43
+ </div>
44
+ <input type="file" id="fileInput" style="height: 0px; width: 0px; border: none; position: absolute" />
45
+ <hr />
46
+ <div id="url-upload-btn">
47
+ <img src="static/assets/link-icon.svg" />
48
+ URL Upload
49
+ </div>
50
+ </div>
51
+
52
+ <div class="sidebar-menu">
53
+ <a class="selected-item" href="/?path=/"><img src="static/assets/home-icon.svg" />Home</a>
54
+ <a class="unselected-item" href="/?path=/trash"><img src="static/assets/trash-icon.svg" />Trash</a>
55
+ </div>
56
+ </div>
57
+ <!-- Sidebar End -->
58
+
59
+ <div id="bg-blur" class="bg-blur"></div>
60
+
61
+ <!-- Create New Folder Start -->
62
+ <div id="create-new-folder" class="create-new-folder">
63
+ <span>New Folder</span>
64
+ <input type="text" id="new-folder-name" placeholder="Enter Folder Name" autocomplete="off" />
65
+ <div>
66
+ <button id="new-folder-cancel">Cancel</button>
67
+ <button id="new-folder-create">Create</button>
68
+ </div>
69
+ </div>
70
+ <!-- Create New Folder End -->
71
+
72
+ <!-- File / Folder Rename Start -->
73
+ <div id="rename-file-folder" class="create-new-folder">
74
+ <span>Edit File/Folder Name</span>
75
+ <input type="text" id="rename-name" placeholder="Enter File/Folder Name" autocomplete="off" />
76
+ <div>
77
+ <button id="rename-cancel">Cancel</button>
78
+ <button id="rename-create">Rename</button>
79
+ </div>
80
+ </div>
81
+ <!-- File / Folder Rename End -->
82
+
83
+ <!-- Remote Upload Start -->
84
+ <div id="new-url-upload" class="create-new-folder">
85
+ <span>Url Upload</span>
86
+ <input type="text" id="remote-url" placeholder="Enter Direct Download Link Of File" autocomplete="off" />
87
+ <div id="single-threaded-div">
88
+ <input type="checkbox" name="single-threaded-toggle" id="single-threaded-toggle">
89
+ <label for="single-threaded-toggle">Single Threaded</label>
90
+ <a href="#"><img src="static/assets/info-icon-small.svg" alt="Info"></a>
91
+ </div>
92
+ <div>
93
+ <button id="remote-cancel">Cancel</button>
94
+ <button id="remote-start">Upload</button>
95
+ </div>
96
+ </div>
97
+ <!-- Remote Upload End -->
98
+
99
+ <!-- Get Password Start -->
100
+ <div id="get-password" class="create-new-folder">
101
+ <span>Admin Login</span>
102
+ <input type="text" id="auth-pass" placeholder="Enter Password" autocomplete="off" />
103
+ <div>
104
+ <button id="pass-login">Login</button>
105
+ </div>
106
+ </div>
107
+ <!-- Get Password End -->
108
+
109
+ <!-- File Uploader Start -->
110
+ <div id="file-uploader" class="file-uploader">
111
+ <span class="upload-head">🚀 Uploading File...</span>
112
+ <span id="upload-filename" class="upload-info">Filename : </span>
113
+ <span id="upload-filesize" class="upload-info">Filesize :</span>
114
+ <span id="upload-status" class="upload-info">Status : </span>
115
+ <span id="upload-percent" class="upload-info">Progress : </span>
116
+ <div class="progress">
117
+ <div class="progress-bar" id="progress-bar"></div>
118
+ </div>
119
+ <div class="btn-div">
120
+ <button id="cancel-file-upload">Cancel Upload</button>
121
+ </div>
122
+ </div>
123
+ <!-- File Uploader End -->
124
+
125
+ <!-- Request Movie Start -->
126
+ <div id="request-movie" class="create-new-folder">
127
+ <span>Request Movie</span>
128
+ <input type="text" id="request-movie-title" placeholder="Movie Title" autocomplete="off" />
129
+ <input type="text" id="request-movie-year" placeholder="Year (e.g. 2023)" autocomplete="off" />
130
+ <div id="request-movie-info" style="font-size: 0.85rem; margin-top: 8px; max-height: 120px; overflow-y: auto;"></div>
131
+ <div style="margin-top: 10px;">
132
+ <button id="request-movie-cancel">Cancel</button>
133
+ <button id="request-movie-fetch">Fetch From TMDB</button>
134
+ <button id="request-movie-send">Request This Movie</button>
135
+ </div>
136
+ </div>
137
+ <!-- Request Movie End -->
138
+
139
+ <!-- Main Content Start -->
140
+ <div class="main-content">
141
+ <div class="header">
142
+ <button id="sidebar-toggle-btn" class="theme-toggle-btn" title="Toggle Sidebar"
143
+ style="margin-right: 10px;">
144
+ <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" fill="#444746">
145
+ <path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" />
146
+ </svg>
147
+ </button>
148
+ <button id="theme-toggle-btn" class="theme-toggle-btn" title="Toggle Theme">
149
+ <svg id="theme-icon" xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"
150
+ fill="#444746">
151
+ <path
152
+ d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-.46-.04-.92-.1-1.36-.98 1.37-2.58 2.26-4.4 2.26-2.98 0-5.4-2.42-5.4-5.4 0-1.81.89-3.42 2.26-4.4-.44-.06-.9-.1-1.36-.1z" />
153
+ </svg>
154
+ </button>
155
+ <div class="sort-dropdown">
156
+ <button id="sort-toggle-btn" class="theme-toggle-btn" title="Sort Files">
157
+ <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" fill="#444746">
158
+ <path d="M3 18h6v-2H3v2zM3 6v2h18V6H3zm0 7h12v-2H3v2z"/>
159
+ </svg>
160
+ </button>
161
+ <div id="sort-menu" class="sort-menu">
162
+ <div class="sort-option" data-sort="date-desc">📅 Newest First</div>
163
+ <div class="sort-option" data-sort="date-asc">📅 Oldest First</div>
164
+ <div class="sort-option" data-sort="name-asc">🔤 A to Z</div>
165
+ <div class="sort-option" data-sort="name-desc">🔤 Z to A</div>
166
+ <div class="sort-option" data-sort="size-desc">📊 Largest First</div>
167
+ <div class="sort-option" data-sort="size-asc">📊 Smallest First</div>
168
+ </div>
169
+ </div>
170
+ <div class="search-bar">
171
+ <img src="static/assets/search-icon.svg" />
172
+ <form id="search-form">
173
+ <input id="file-search" type="text" placeholder="Search in Drive" autocomplete="off" />
174
+ </form>
175
+ </div>
176
+
177
+ <button id="request-movie-btn" class="theme-toggle-btn" title="Request Movie" style="margin-left: 10px;">
178
+ Request Movie
179
+ </button>
180
+ </div>
181
+
182
+ <div class="directory">
183
+ <table>
184
+ <thead>
185
+ <tr>
186
+ <th>Name</th>
187
+ <th>File Size</th>
188
+ <th>More</th>
189
+ </tr>
190
+ </thead>
191
+ <tbody id="directory-data"></tbody>
192
+ </table>
193
+ </div>
194
+ </div>
195
+ </div>
196
+
197
+ <!-- Toast Notifications Container -->
198
+ <div id="toast-container"></div>
199
+
200
+ <!-- Loading Overlay -->
201
+ <div id="loading-overlay" class="loading-overlay">
202
+ <div class="loading-spinner">
203
+ <div class="spinner-ring"></div>
204
+ <div class="spinner-ring"></div>
205
+ <div class="spinner-ring"></div>
206
+ </div>
207
+ <p id="loading-text">Loading...</p>
208
+ </div>
209
+
210
+ <script src="static/js/toast.js?v=3"></script>
211
+ <script src="static/js/extra.js?v=2"></script>
212
+ <script src="static/js/apiHandler.js?v=2"></script>
213
+ <script src="static/js/sidebar.js?v=2"></script>
214
+ <script src="static/js/fileClickHandler.js?v=2"></script>
215
+ <script src="static/js/sorting.js?v=3"></script>
216
+ <script src="static/js/main.js?v=2"></script>
217
+ </body>
218
+
219
+ </html>
website/static/assets/file-icon.svg ADDED
website/static/assets/folder-icon.svg ADDED
website/static/assets/folder-solid-icon.svg ADDED
website/static/assets/home-icon.svg ADDED
website/static/assets/info-icon-small.svg ADDED
website/static/assets/link-icon.svg ADDED
website/static/assets/load-icon.svg ADDED
website/static/assets/more-icon.svg ADDED
website/static/assets/pencil-icon.svg ADDED
website/static/assets/plus-icon.svg ADDED
website/static/assets/profile-icon.svg ADDED
website/static/assets/search-icon.svg ADDED
website/static/assets/share-icon.svg ADDED
website/static/assets/trash-icon.svg ADDED
website/static/assets/upload-icon.svg ADDED
website/static/home.css ADDED
@@ -0,0 +1,1112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0px;
3
+ padding: 0px;
4
+ box-sizing: border-box;
5
+ font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
6
+ user-select: none;
7
+ -webkit-user-select: none;
8
+ -moz-user-select: none;
9
+ -ms-user-select: none;
10
+ }
11
+
12
+ /* Smooth scrolling */
13
+ html {
14
+ scroll-behavior: smooth;
15
+ }
16
+
17
+ /* Gen-Z Animation keyframes */
18
+ @keyframes slideInUp {
19
+ from {
20
+ opacity: 0;
21
+ transform: translateY(20px);
22
+ }
23
+ to {
24
+ opacity: 1;
25
+ transform: translateY(0);
26
+ }
27
+ }
28
+
29
+ @keyframes slideInDown {
30
+ from {
31
+ opacity: 0;
32
+ transform: translateY(-20px);
33
+ }
34
+ to {
35
+ opacity: 1;
36
+ transform: translateY(0);
37
+ }
38
+ }
39
+
40
+ @keyframes fadeIn {
41
+ from {
42
+ opacity: 0;
43
+ }
44
+ to {
45
+ opacity: 1;
46
+ }
47
+ }
48
+
49
+ @keyframes scaleIn {
50
+ from {
51
+ opacity: 0;
52
+ transform: scale(0.9);
53
+ }
54
+ to {
55
+ opacity: 1;
56
+ transform: scale(1);
57
+ }
58
+ }
59
+
60
+ @keyframes spin {
61
+ to {
62
+ transform: rotate(360deg);
63
+ }
64
+ }
65
+
66
+ .container {
67
+ width: 100%;
68
+ height: 100vh;
69
+ display: grid;
70
+ background: #f1f1f1;
71
+ grid-template-columns: auto 1fr;
72
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
73
+ }
74
+
75
+ .rotate-90 {
76
+ transform: rotate(90deg);
77
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
78
+ }
79
+
80
+ /* Sidebar Style Start */
81
+
82
+ .sidebar {
83
+ width: 250px;
84
+ height: 100%;
85
+ background-color: #f8fafd;
86
+ padding: 20px;
87
+ display: flex;
88
+ flex-direction: column;
89
+ justify-content: start;
90
+ align-items: start;
91
+ }
92
+
93
+ .sidebar-header {
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: start;
97
+ }
98
+
99
+ .sidebar-header img {
100
+ width: 40px;
101
+ height: 40px;
102
+ margin-right: 10px;
103
+ }
104
+
105
+ .sidebar-header span {
106
+ font-size: 1.2rem;
107
+ font-weight: 500;
108
+ color: #444746;
109
+ }
110
+
111
+ .sidebar .new-button {
112
+ padding: 15px 20px;
113
+ background-color: #fff;
114
+ border: 1px solid #e0e0e0;
115
+ border-radius: 20px;
116
+ margin-top: 20px;
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: center;
120
+ cursor: pointer;
121
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
122
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
123
+ font-size: 0.9rem;
124
+ color: #282c35;
125
+ font-weight: 500;
126
+ position: relative;
127
+ overflow: hidden;
128
+ }
129
+
130
+ .sidebar .new-button::before {
131
+ content: '';
132
+ position: absolute;
133
+ top: 50%;
134
+ left: 50%;
135
+ width: 0;
136
+ height: 0;
137
+ border-radius: 50%;
138
+ background: rgba(102, 126, 234, 0.1);
139
+ transform: translate(-50%, -50%);
140
+ transition: width 0.6s, height 0.6s;
141
+ }
142
+
143
+ .sidebar .new-button:hover::before {
144
+ width: 300px;
145
+ height: 300px;
146
+ }
147
+
148
+ .sidebar .new-button:hover {
149
+ background-color: #edf1fa;
150
+ border: 1px solid #c9d0e6;
151
+ box-shadow: 0px 5px 15px rgba(102, 126, 234, 0.3);
152
+ transform: translateY(-2px);
153
+ }
154
+
155
+ .sidebar .new-button img {
156
+ width: 24px;
157
+ height: 24px;
158
+ margin-right: 10px;
159
+ }
160
+
161
+ .new-upload {
162
+ position: absolute;
163
+ background-color: #ffffff;
164
+ padding: 5px 0px;
165
+ box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
166
+ border-radius: 5px;
167
+ width: 250px;
168
+ top: 40px;
169
+ z-index: -1;
170
+ opacity: 0;
171
+ transition: all 0.2s;
172
+ }
173
+
174
+ .new-upload div {
175
+ padding: 10px 20px;
176
+ display: flex;
177
+ align-items: center;
178
+ justify-content: start;
179
+ cursor: pointer;
180
+ transition: all 0.3s ease;
181
+ font-size: 14px;
182
+ }
183
+
184
+ .new-upload div:hover {
185
+ background-color: #f1f1f1;
186
+ }
187
+
188
+ .new-upload div img {
189
+ margin-right: 10px;
190
+ height: 20px;
191
+ width: 20px;
192
+ }
193
+
194
+ .new-upload hr {
195
+ border: 1px solid #e0e0e0;
196
+ margin: 5px 0px;
197
+ }
198
+
199
+ .bg-blur {
200
+ position: fixed;
201
+ background-color: #000000;
202
+ height: 100dvh;
203
+ width: 100dvw;
204
+ transition: opacity 0.3s ease;
205
+ z-index: -1;
206
+ opacity: 0;
207
+ }
208
+
209
+ /* More Options Start */
210
+
211
+ .more-options {
212
+ position: absolute;
213
+ background-color: #ffffff;
214
+ padding: 2px 0px;
215
+ box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
216
+ border-radius: 5px;
217
+ z-index: -1;
218
+ opacity: 0;
219
+ transition: all 0.2s;
220
+ width: 150px;
221
+ transform: translateX(-50%);
222
+ }
223
+
224
+ .more-options div {
225
+ width: 100%;
226
+ padding: 5px 20px;
227
+ display: flex;
228
+ align-items: center;
229
+ justify-content: center;
230
+ cursor: pointer;
231
+ transition: all 0.3s ease;
232
+ font-size: 14px;
233
+ }
234
+
235
+ .more-options div:hover {
236
+ background-color: #f1f1f1;
237
+ }
238
+
239
+ .more-options div img {
240
+ margin-right: 10px;
241
+ height: 20px;
242
+ width: 20px;
243
+ }
244
+
245
+ .more-options hr {
246
+ border: 1px solid #e0e0e0;
247
+ margin: 2px 0px;
248
+ }
249
+
250
+ /* More Options End */
251
+
252
+
253
+ /* Create New Folder Start */
254
+ .create-new-folder {
255
+ position: fixed;
256
+ top: 50%;
257
+ left: 50%;
258
+ transform: translate(-50%, -50%);
259
+ padding: 20px;
260
+ display: flex;
261
+ align-items: center;
262
+ justify-content: center;
263
+ flex-direction: column;
264
+ background-color: #fff;
265
+ border-radius: 10px;
266
+ z-index: -1;
267
+ opacity: 0;
268
+ box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3);
269
+ transition: opacity 0.3s ease;
270
+ width: 400px;
271
+ }
272
+
273
+ .create-new-folder span {
274
+ font-size: 1.2rem;
275
+ font-weight: 400;
276
+ color: #444746;
277
+ margin-bottom: 20px;
278
+ width: 100%;
279
+ margin-left: 10px;
280
+ }
281
+
282
+ .create-new-folder input {
283
+ width: 100%;
284
+ padding: 10px 20px;
285
+ border: 1px solid #e0e0e0;
286
+ border-radius: 5px;
287
+ outline: none;
288
+ font-size: 0.9rem;
289
+ font-weight: 400;
290
+ color: #444746;
291
+ margin-bottom: 20px;
292
+ }
293
+
294
+ .create-new-folder div {
295
+ width: 100%;
296
+ display: flex;
297
+ align-items: center;
298
+ justify-content: end;
299
+ gap: 10px;
300
+ }
301
+
302
+ .create-new-folder button {
303
+ background-color: transparent;
304
+ border: none;
305
+ border-radius: 20px;
306
+ display: flex;
307
+ align-items: center;
308
+ justify-content: center;
309
+ cursor: pointer;
310
+ transition: all 0.3s ease;
311
+ font-size: 0.9rem;
312
+ color: #0b57d0;
313
+ font-weight: 500;
314
+ padding: 10px 20px;
315
+ }
316
+
317
+ .create-new-folder button:hover {
318
+ background-color: #edf1fa;
319
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2);
320
+ }
321
+
322
+ /* Create New Folder End */
323
+
324
+ /* Remote URL Upload Start */
325
+
326
+ #single-threaded-div {
327
+ display: flex;
328
+ flex-direction: row;
329
+ justify-content: start;
330
+ align-items: center;
331
+ width: 100%;
332
+ margin: 10px 0px;
333
+ }
334
+
335
+ #single-threaded-div input {
336
+ width: auto;
337
+ margin: 0px;
338
+ margin-left: 10px;
339
+ }
340
+
341
+ #single-threaded-div img {
342
+ height: 16px;
343
+ width: 16px;
344
+ }
345
+
346
+ #single-threaded-div a {
347
+ height: 16px;
348
+ width: 16px;
349
+ }
350
+
351
+ /* Remote URL Upload End */
352
+
353
+ /* File Uploader Start */
354
+
355
+ .file-uploader {
356
+ position: fixed;
357
+ top: 50%;
358
+ left: 50%;
359
+ transform: translate(-50%, -50%);
360
+ padding: 20px;
361
+ display: flex;
362
+ align-items: center;
363
+ justify-content: center;
364
+ flex-direction: column;
365
+ background-color: #fff;
366
+ border-radius: 10px;
367
+ z-index: -1;
368
+ box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3);
369
+ transition: opacity 0.3s ease;
370
+ width: 500px;
371
+ opacity: 0;
372
+ }
373
+
374
+ .upload-head {
375
+ font-size: 1.2rem;
376
+ font-weight: 500;
377
+ color: #444746;
378
+ margin-bottom: 20px;
379
+ width: 100%;
380
+ }
381
+
382
+ .upload-info {
383
+ font-size: 16px;
384
+ font-weight: 400;
385
+ color: #444746;
386
+ margin-bottom: 5px;
387
+ width: 100%;
388
+ max-height: 40px;
389
+ overflow: hidden;
390
+ }
391
+
392
+ .progress {
393
+ width: 100%;
394
+ background-color: #ddd;
395
+ border-radius: 5px;
396
+ overflow: hidden;
397
+ margin-top: 20px;
398
+ }
399
+
400
+ .progress-bar {
401
+ height: 20px;
402
+ background-color: #007bff;
403
+ width: 0;
404
+ transition: width 0.3s;
405
+ }
406
+
407
+ .file-uploader .btn-div {
408
+ width: 100%;
409
+ display: flex;
410
+ align-items: center;
411
+ justify-content: end;
412
+ margin-top: 20px;
413
+ }
414
+
415
+ .file-uploader button {
416
+ background-color: transparent;
417
+ border: none;
418
+ border-radius: 20px;
419
+ display: flex;
420
+ align-items: center;
421
+ justify-content: center;
422
+ cursor: pointer;
423
+ transition: all 0.3s ease;
424
+ font-size: 0.9rem;
425
+ color: #0b57d0;
426
+ font-weight: 500;
427
+ padding: 10px 20px;
428
+ }
429
+
430
+ .file-uploader button:hover {
431
+ background-color: #edf1fa;
432
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2);
433
+ }
434
+
435
+ /* File Uploader End */
436
+
437
+ .sidebar-menu {
438
+ margin-top: 20px;
439
+ width: 100%;
440
+ display: flex;
441
+ flex-direction: column;
442
+ justify-content: start;
443
+ align-items: center;
444
+ }
445
+
446
+ .sidebar-menu a {
447
+ width: 100%;
448
+ padding: 10px 20px;
449
+ display: flex;
450
+ align-items: center;
451
+ justify-content: start;
452
+ color: #444746;
453
+ font-size: 0.9rem;
454
+ font-weight: 400;
455
+ text-decoration: none;
456
+ transition: all 0.3s ease;
457
+ border-radius: 20px;
458
+ }
459
+
460
+ .sidebar-menu .selected-item {
461
+ background-color: #c2e7ff;
462
+ }
463
+
464
+ .sidebar-menu .unselected-item:hover {
465
+ background-color: #cfcfcf;
466
+ }
467
+
468
+ .sidebar-menu a img {
469
+ width: 20px;
470
+ height: 20px;
471
+ margin-right: 10px;
472
+ }
473
+
474
+ /* Sidebar Style End */
475
+
476
+ /* Main Content Style Start */
477
+
478
+ .main-content {
479
+ width: 100%;
480
+ height: 100%;
481
+ padding: 20px;
482
+ display: flex;
483
+ flex-direction: column;
484
+ justify-content: start;
485
+ align-items: start;
486
+ background-color: #f8fafd;
487
+ }
488
+
489
+
490
+ .main-content .header {
491
+ width: 100%;
492
+ display: flex;
493
+ align-items: center;
494
+ gap: 15px;
495
+ }
496
+
497
+ #search-form {
498
+ height: 100%;
499
+ width: 100%;
500
+ }
501
+
502
+ .search-bar {
503
+ width: 100%;
504
+ display: flex;
505
+ align-items: center;
506
+ justify-content: start;
507
+ background-color: #e9eef6;
508
+ border-radius: 20px;
509
+ padding: 10px 20px;
510
+ flex-grow: 1;
511
+ }
512
+
513
+ .search-bar img {
514
+ width: 20px;
515
+ height: 20px;
516
+ margin-right: 20px;
517
+ }
518
+
519
+ .search-bar input {
520
+ width: 100%;
521
+ border: none;
522
+ outline: none;
523
+ background-color: transparent;
524
+ font-size: 1rem;
525
+ font-weight: 400;
526
+ }
527
+
528
+ .search-bar:focus-within {
529
+ background-color: #ffffff;
530
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
531
+ }
532
+
533
+ .directory {
534
+ background-color: #fff;
535
+ padding: 10px 20px;
536
+ width: 100%;
537
+ height: 100%;
538
+ border-radius: 20px;
539
+ margin-top: 20px;
540
+ }
541
+
542
+ .directory table {
543
+ width: 100%;
544
+ border-collapse: collapse;
545
+ }
546
+
547
+ .directory table tr th {
548
+ text-align: start;
549
+ font-size: 0.9rem;
550
+ font-weight: 500;
551
+ color: #444746;
552
+ padding: 10px 0px;
553
+ border-bottom: 1px solid #e0e0e0;
554
+ }
555
+
556
+ .directory table tr td {
557
+ text-align: start;
558
+ font-size: 0.9rem;
559
+ font-weight: 400;
560
+ color: #444746;
561
+ border-bottom: 1px solid #e0e0e0;
562
+ height: 50px;
563
+ }
564
+
565
+ .directory .body-tr {
566
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
567
+ cursor: pointer;
568
+ }
569
+
570
+ .directory .body-tr:hover {
571
+ background-color: #f1f1f1;
572
+ transform: scale(1.01);
573
+ }
574
+
575
+ .directory .td-align {
576
+ display: flex;
577
+ align-items: center;
578
+ justify-content: start;
579
+ width: 100%;
580
+ height: 100%;
581
+ }
582
+
583
+ .directory .td-align img {
584
+ width: 24px;
585
+ height: 24px;
586
+ margin-right: 10px;
587
+ }
588
+
589
+ .directory .more-btn img {
590
+ width: 14px;
591
+ height: 14px;
592
+ margin: 0px;
593
+ }
594
+
595
+ .directory .more-btn {
596
+ padding: 8px;
597
+ border-radius: 20px;
598
+ display: flex;
599
+ align-items: center;
600
+ justify-content: center;
601
+ cursor: pointer;
602
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
603
+ }
604
+
605
+ .directory .more-btn:hover {
606
+ background-color: #b9b9b9;
607
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
608
+ transform: rotate(90deg) scale(1.1);
609
+ }
610
+
611
+ /* Theme Toggle & Dark Mode */
612
+ /* Theme Toggle & Dark Mode */
613
+
614
+
615
+ .theme-toggle-btn {
616
+ background: none;
617
+ border: none;
618
+ cursor: pointer;
619
+ border-radius: 50%;
620
+ padding: 8px;
621
+ width: 40px;
622
+ height: 40px;
623
+ display: flex;
624
+ align-items: center;
625
+ justify-content: center;
626
+ transition: background-color 0.3s;
627
+ flex-shrink: 0;
628
+ }
629
+
630
+ .theme-toggle-btn:hover {
631
+ background-color: rgba(0, 0, 0, 0.1);
632
+ }
633
+
634
+ body.dark-mode .theme-toggle-btn:hover {
635
+ background-color: rgba(255, 255, 255, 0.1);
636
+ }
637
+
638
+ /* Dark Mode Variables/Overrides */
639
+ body.dark-mode .container {
640
+ background-color: #121212;
641
+ }
642
+
643
+ body.dark-mode .sidebar {
644
+ background-color: #1e1e1e;
645
+ border-right: 1px solid #333;
646
+ }
647
+
648
+ body.dark-mode .sidebar-header span {
649
+ color: #e0e0e0;
650
+ }
651
+
652
+ body.dark-mode .sidebar .new-button {
653
+ background-color: #2d2d2d;
654
+ color: #e0e0e0;
655
+ border: 1px solid #444;
656
+ }
657
+
658
+ body.dark-mode .sidebar .new-button:hover {
659
+ background-color: #383838;
660
+ }
661
+
662
+ body.dark-mode .new-upload {
663
+ background-color: #2d2d2d;
664
+ box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5);
665
+ }
666
+
667
+ body.dark-mode .new-upload div {
668
+ color: #e0e0e0;
669
+ }
670
+
671
+ body.dark-mode .new-upload div:hover {
672
+ background-color: #383838;
673
+ }
674
+
675
+ body.dark-mode .sidebar-menu a {
676
+ color: #bfbfbf;
677
+ }
678
+
679
+ body.dark-mode .sidebar-menu .selected-item {
680
+ background-color: #3a4a5e;
681
+ color: #fff;
682
+ }
683
+
684
+ body.dark-mode .sidebar-menu .unselected-item:hover {
685
+ background-color: #2d2d2d;
686
+ }
687
+
688
+ /* Sidebar Collapsed State */
689
+ .container.sidebar-collapsed {
690
+ grid-template-columns: 0px 1fr;
691
+ }
692
+
693
+ .container.sidebar-collapsed .sidebar {
694
+ padding: 0px;
695
+ overflow: hidden;
696
+ width: 0px;
697
+ }
698
+
699
+ body.dark-mode .main-content {
700
+ background-color: #121212;
701
+ }
702
+
703
+ body.dark-mode .search-bar {
704
+ background-color: #2d2d2d;
705
+ }
706
+
707
+ body.dark-mode .search-bar input {
708
+ color: #e0e0e0;
709
+ }
710
+
711
+ body.dark-mode .directory {
712
+ background-color: #1e1e1e;
713
+ }
714
+
715
+ body.dark-mode .directory table tr th {
716
+ color: #aaa;
717
+ border-bottom: 1px solid #333;
718
+ }
719
+
720
+ body.dark-mode .directory table tr td {
721
+ color: #e0e0e0;
722
+ border-bottom: 1px solid #333;
723
+ }
724
+
725
+ body.dark-mode .directory .body-tr:hover {
726
+ background-color: #2d2d2d;
727
+ }
728
+
729
+ body.dark-mode .create-new-folder,
730
+ body.dark-mode .file-uploader,
731
+ body.dark-mode .more-options,
732
+ body.dark-mode #get-password {
733
+ background-color: #1e1e1e;
734
+ color: #e0e0e0;
735
+ }
736
+
737
+ body.dark-mode .create-new-folder input,
738
+ body.dark-mode #auth-pass {
739
+ background-color: #2d2d2d;
740
+ border: 1px solid #444;
741
+ color: #e0e0e0;
742
+ }
743
+
744
+ body.dark-mode .create-new-folder span,
745
+ body.dark-mode .upload-head,
746
+ body.dark-mode .upload-info {
747
+ color: #e0e0e0;
748
+ }
749
+
750
+ body.dark-mode .more-options div:hover {
751
+ background-color: #383838;
752
+ }
753
+
754
+ body.dark-mode .more-options div {
755
+ color: #e0e0e0;
756
+ }
757
+
758
+ /* Icon Inversion for Dark Mode */
759
+ body.dark-mode img {
760
+ filter: invert(0.9);
761
+ }
762
+
763
+ /* Exceptions for images that shouldn't be inverted */
764
+ body.dark-mode .sidebar-header img,
765
+ /* Logo */
766
+ body.dark-mode .directory img[src*="file-icon"],
767
+ /* Maybe file icon colors matter? assuming svgs are mono for now */
768
+ body.dark-mode .directory img[src*="folder-solid"] {
769
+ filter: none;
770
+ }
771
+
772
+ /* Re-invert folder icon if it is dark? */
773
+ body.dark-mode .directory img[src*="folder-solid"] {
774
+ filter: invert(0.7) sepia(1) hue-rotate(180deg);
775
+ /* Attempt to make it bluish or standard folder color if it was black */
776
+ }
777
+
778
+ /* Ideally, we should check what icons look like. Assuming black icons, invert 0.9 makes them white. */
779
+ body.dark-mode .directory img[src*="folder-solid-icon.svg"] {
780
+ filter: invert(0.5);
781
+ /* Grey */
782
+ }
783
+
784
+ /* SVG Fill */
785
+ body.dark-mode svg {
786
+ fill: #e0e0e0 !important;
787
+ }
788
+
789
+ /* Toast Notification System */
790
+ #toast-container {
791
+ position: fixed;
792
+ top: 20px;
793
+ right: 20px;
794
+ z-index: 10000;
795
+ display: flex;
796
+ flex-direction: column;
797
+ gap: 12px;
798
+ pointer-events: none;
799
+ }
800
+
801
+ .toast {
802
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
803
+ color: white;
804
+ padding: 16px 24px;
805
+ border-radius: 16px;
806
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
807
+ display: flex;
808
+ align-items: center;
809
+ gap: 12px;
810
+ min-width: 300px;
811
+ max-width: 400px;
812
+ animation: slideInDown 0.3s cubic-bezier(0.4, 0, 0.2, 1);
813
+ backdrop-filter: blur(10px);
814
+ pointer-events: all;
815
+ font-weight: 500;
816
+ font-size: 14px;
817
+ position: relative;
818
+ overflow: hidden;
819
+ }
820
+
821
+ .toast::before {
822
+ content: '';
823
+ position: absolute;
824
+ top: 0;
825
+ left: 0;
826
+ width: 100%;
827
+ height: 3px;
828
+ background: rgba(255, 255, 255, 0.5);
829
+ animation: toastProgress 3s linear;
830
+ }
831
+
832
+ @keyframes toastProgress {
833
+ from {
834
+ width: 100%;
835
+ }
836
+ to {
837
+ width: 0%;
838
+ }
839
+ }
840
+
841
+ .toast.success {
842
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
843
+ }
844
+
845
+ .toast.error {
846
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
847
+ }
848
+
849
+ .toast.info {
850
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
851
+ }
852
+
853
+ .toast.warning {
854
+ background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
855
+ }
856
+
857
+ .toast-icon {
858
+ font-size: 24px;
859
+ flex-shrink: 0;
860
+ }
861
+
862
+ .toast-message {
863
+ flex: 1;
864
+ line-height: 1.4;
865
+ }
866
+
867
+ .toast-close {
868
+ background: rgba(255, 255, 255, 0.2);
869
+ border: none;
870
+ color: white;
871
+ width: 24px;
872
+ height: 24px;
873
+ border-radius: 50%;
874
+ cursor: pointer;
875
+ display: flex;
876
+ align-items: center;
877
+ justify-content: center;
878
+ flex-shrink: 0;
879
+ transition: background 0.2s;
880
+ font-size: 18px;
881
+ line-height: 1;
882
+ }
883
+
884
+ .toast-close:hover {
885
+ background: rgba(255, 255, 255, 0.3);
886
+ }
887
+
888
+ .toast.hiding {
889
+ animation: slideOutRight 0.3s cubic-bezier(0.4, 0, 0.2, 1);
890
+ }
891
+
892
+ @keyframes slideOutRight {
893
+ to {
894
+ opacity: 0;
895
+ transform: translateX(400px);
896
+ }
897
+ }
898
+
899
+ body.dark-mode .toast {
900
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
901
+ }
902
+
903
+ /* Loading Overlay */
904
+ .loading-overlay {
905
+ position: fixed;
906
+ top: 0;
907
+ left: 0;
908
+ width: 100vw;
909
+ height: 100vh;
910
+ background: rgba(0, 0, 0, 0.7);
911
+ backdrop-filter: blur(8px);
912
+ display: none;
913
+ align-items: center;
914
+ justify-content: center;
915
+ flex-direction: column;
916
+ z-index: 9999;
917
+ animation: fadeIn 0.3s;
918
+ }
919
+
920
+ .loading-overlay.show {
921
+ display: flex;
922
+ }
923
+
924
+ .loading-spinner {
925
+ position: relative;
926
+ width: 80px;
927
+ height: 80px;
928
+ }
929
+
930
+ .spinner-ring {
931
+ position: absolute;
932
+ width: 100%;
933
+ height: 100%;
934
+ border: 4px solid transparent;
935
+ border-radius: 50%;
936
+ animation: spin 1.5s cubic-bezier(0.5, 0, 0.5, 1) infinite;
937
+ }
938
+
939
+ .spinner-ring:nth-child(1) {
940
+ border-top-color: #667eea;
941
+ animation-delay: -0.45s;
942
+ }
943
+
944
+ .spinner-ring:nth-child(2) {
945
+ border-top-color: #764ba2;
946
+ animation-delay: -0.3s;
947
+ }
948
+
949
+ .spinner-ring:nth-child(3) {
950
+ border-top-color: #f093fb;
951
+ animation-delay: -0.15s;
952
+ }
953
+
954
+ #loading-text {
955
+ color: white;
956
+ margin-top: 20px;
957
+ font-size: 16px;
958
+ font-weight: 500;
959
+ animation: fadeIn 0.5s;
960
+ }
961
+
962
+ /* Sort Dropdown */
963
+ .sort-dropdown {
964
+ position: relative;
965
+ }
966
+
967
+ .sort-menu {
968
+ position: absolute;
969
+ top: 45px;
970
+ right: 0;
971
+ background: white;
972
+ border-radius: 12px;
973
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
974
+ padding: 8px;
975
+ min-width: 200px;
976
+ opacity: 0;
977
+ visibility: hidden;
978
+ transform: translateY(-10px);
979
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
980
+ z-index: 100;
981
+ }
982
+
983
+ .sort-menu.show {
984
+ opacity: 1;
985
+ visibility: visible;
986
+ transform: translateY(0);
987
+ }
988
+
989
+ .sort-option {
990
+ padding: 12px 16px;
991
+ border-radius: 8px;
992
+ cursor: pointer;
993
+ transition: all 0.2s;
994
+ font-size: 14px;
995
+ font-weight: 500;
996
+ color: #444746;
997
+ display: flex;
998
+ align-items: center;
999
+ gap: 10px;
1000
+ }
1001
+
1002
+ .sort-option:hover {
1003
+ background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
1004
+ transform: translateX(4px);
1005
+ }
1006
+
1007
+ .sort-option.active {
1008
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1009
+ color: white;
1010
+ }
1011
+
1012
+ body.dark-mode .sort-menu {
1013
+ background: #2d2d2d;
1014
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
1015
+ }
1016
+
1017
+ body.dark-mode .sort-option {
1018
+ color: #e0e0e0;
1019
+ }
1020
+
1021
+ body.dark-mode .sort-option:hover {
1022
+ background: #383838;
1023
+ }
1024
+
1025
+ body.dark-mode .sort-option.active {
1026
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1027
+ color: white;
1028
+ }
1029
+
1030
+ /* Mobile Responsive Styles */
1031
+ @media screen and (max-width: 768px) {
1032
+ .container {
1033
+ grid-template-columns: 1fr;
1034
+ }
1035
+
1036
+ .sidebar {
1037
+ position: fixed;
1038
+ left: -100%;
1039
+ top: 0;
1040
+ height: 100vh;
1041
+ z-index: 100;
1042
+ transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1043
+ box-shadow: 2px 0 20px rgba(0, 0, 0, 0.1);
1044
+ }
1045
+
1046
+ .sidebar.mobile-open {
1047
+ left: 0;
1048
+ }
1049
+
1050
+ .main-content {
1051
+ padding: 10px;
1052
+ }
1053
+
1054
+ .header {
1055
+ flex-wrap: wrap;
1056
+ gap: 10px;
1057
+ }
1058
+
1059
+ .search-bar {
1060
+ order: 3;
1061
+ width: 100%;
1062
+ margin-top: 10px;
1063
+ }
1064
+
1065
+ .directory {
1066
+ overflow-x: auto;
1067
+ border-radius: 12px;
1068
+ }
1069
+
1070
+ .directory table {
1071
+ min-width: 500px;
1072
+ }
1073
+
1074
+ .create-new-folder,
1075
+ .file-uploader {
1076
+ width: 90%;
1077
+ max-width: 400px;
1078
+ }
1079
+
1080
+ #toast-container {
1081
+ right: 10px;
1082
+ left: 10px;
1083
+ top: 10px;
1084
+ }
1085
+
1086
+ .toast {
1087
+ min-width: auto;
1088
+ max-width: 100%;
1089
+ }
1090
+ }
1091
+
1092
+ @media screen and (max-width: 480px) {
1093
+ .sidebar-header span {
1094
+ font-size: 1rem;
1095
+ }
1096
+
1097
+ .new-button {
1098
+ font-size: 0.8rem;
1099
+ padding: 12px 16px !important;
1100
+ }
1101
+
1102
+ .directory table tr th,
1103
+ .directory table tr td {
1104
+ font-size: 0.8rem;
1105
+ padding: 8px 5px;
1106
+ }
1107
+
1108
+ .td-align img {
1109
+ width: 20px !important;
1110
+ height: 20px !important;
1111
+ }
1112
+ }
website/static/js/apiHandler.js ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Api Fuctions
2
+ async function postJson(url, data) {
3
+ data['password'] = getPassword()
4
+ const response = await fetch(url, {
5
+ method: 'POST',
6
+ headers: {
7
+ 'Content-Type': 'application/json'
8
+ },
9
+ body: JSON.stringify(data)
10
+ })
11
+ return await response.json()
12
+ }
13
+
14
+ document.getElementById('pass-login').addEventListener('click', async () => {
15
+ const password = document.getElementById('auth-pass').value
16
+ const data = { 'pass': password }
17
+ showLoading('Logging in...');
18
+ const json = await postJson('/api/checkPassword', data)
19
+ hideLoading();
20
+ if (json.status === 'ok') {
21
+ localStorage.setItem('password', password)
22
+ showToast('🎉 Login successful!', 'success');
23
+ setTimeout(() => window.location.reload(), 1000);
24
+ }
25
+ else {
26
+ showToast('❌ Wrong password', 'error');
27
+ }
28
+
29
+ })
30
+
31
+ async function getCurrentDirectory() {
32
+ let path = getCurrentPath()
33
+ if (path === 'redirect') {
34
+ return
35
+ }
36
+ try {
37
+ showLoading('Loading directory...');
38
+ const auth = getFolderAuthFromPath()
39
+ console.log(path)
40
+
41
+ const data = { 'path': path, 'auth': auth }
42
+ const json = await postJson('/api/getDirectory', data)
43
+ hideLoading();
44
+
45
+ if (json.status === 'ok') {
46
+ if (getCurrentPath().startsWith('/share')) {
47
+ const sections = document.querySelector('.sidebar-menu').getElementsByTagName('a')
48
+ console.log(path)
49
+
50
+ if (removeSlash(json['auth_home_path']) === removeSlash(path.split('_')[1])) {
51
+ sections[0].setAttribute('class', 'selected-item')
52
+
53
+ } else {
54
+ sections[0].setAttribute('class', 'unselected-item')
55
+ }
56
+ sections[0].href = `/?path=/share_${removeSlash(json['auth_home_path'])}&auth=${auth}`
57
+ console.log(`/?path=/share_${removeSlash(json['auth_home_path'])}&auth=${auth}`)
58
+ }
59
+
60
+ console.log(json)
61
+ showDirectory(json['data'])
62
+ } else {
63
+ showToast('❌ Directory not found', 'error');
64
+ }
65
+ }
66
+ catch (err) {
67
+ console.log(err)
68
+ hideLoading();
69
+ showToast('❌ Directory not found', 'error');
70
+ }
71
+ }
72
+
73
+ async function createNewFolder() {
74
+ const folderName = document.getElementById('new-folder-name').value;
75
+ const path = getCurrentPath()
76
+ if (path === 'redirect') {
77
+ return
78
+ }
79
+ if (folderName.length > 0) {
80
+ const data = {
81
+ 'name': folderName,
82
+ 'path': path
83
+ }
84
+ try {
85
+ showLoading('Creating folder...');
86
+ const json = await postJson('/api/createNewFolder', data)
87
+ hideLoading();
88
+
89
+ if (json.status === 'ok') {
90
+ showToast('📁 Folder created successfully!', 'success');
91
+ setTimeout(() => window.location.reload(), 1000);
92
+ } else {
93
+ showToast(json.status, 'error');
94
+ }
95
+ }
96
+ catch (err) {
97
+ hideLoading();
98
+ showToast('❌ Error creating folder', 'error');
99
+ }
100
+ } else {
101
+ showToast('❌ Folder name cannot be empty', 'error');
102
+ }
103
+ }
104
+
105
+
106
+ async function getFolderShareAuth(path) {
107
+ const data = { 'path': path }
108
+ const json = await postJson('/api/getFolderShareAuth', data)
109
+ if (json.status === 'ok') {
110
+ return json.auth
111
+ } else {
112
+ showToast('❌ Error getting folder share link', 'error');
113
+ }
114
+ }
115
+
116
+ async function tmdbSearchMovie(title, year) {
117
+ const data = { 'title': title, 'year': year }
118
+ const json = await postJson('/api/tmdbSearchMovie', data)
119
+ return json
120
+ }
121
+
122
+ async function requestMovie(title, year) {
123
+ const data = { 'title': title, 'year': year }
124
+ const json = await postJson('/api/requestMovie', data)
125
+ return json
126
+ }
127
+
128
+ // File Uploader Start
129
+
130
+ const MAX_FILE_SIZE = MAX_FILE_SIZE__SDGJDG // Will be replaced by the python
131
+
132
+ const fileInput = document.getElementById('fileInput');
133
+ const progressBar = document.getElementById('progress-bar');
134
+ const cancelButton = document.getElementById('cancel-file-upload');
135
+ const uploadPercent = document.getElementById('upload-percent');
136
+ let uploadRequest = null;
137
+ let uploadStep = 0;
138
+ let uploadID = null;
139
+
140
+ fileInput.addEventListener('change', async (e) => {
141
+ const file = fileInput.files[0];
142
+
143
+ if (file.size > MAX_FILE_SIZE) {
144
+ showToast(`❌ File size exceeds ${(MAX_FILE_SIZE / (1024 * 1024 * 1024)).toFixed(2)} GB limit`, 'error');
145
+ return;
146
+ }
147
+
148
+ // Showing file uploader
149
+ document.getElementById('bg-blur').style.zIndex = '2';
150
+ document.getElementById('bg-blur').style.opacity = '0.1';
151
+ document.getElementById('file-uploader').style.zIndex = '3';
152
+ document.getElementById('file-uploader').style.opacity = '1';
153
+
154
+ document.getElementById('upload-filename').innerText = 'Filename: ' + file.name;
155
+ document.getElementById('upload-filesize').innerText = 'Filesize: ' + (file.size / (1024 * 1024)).toFixed(2) + ' MB';
156
+ document.getElementById('upload-status').innerText = 'Status: Uploading To Backend Server';
157
+
158
+
159
+ const formData = new FormData();
160
+ formData.append('file', file);
161
+ formData.append('path', getCurrentPath());
162
+ formData.append('password', getPassword());
163
+ const id = getRandomId();
164
+ formData.append('id', id);
165
+ formData.append('total_size', file.size);
166
+
167
+ uploadStep = 1;
168
+ uploadRequest = new XMLHttpRequest();
169
+ uploadRequest.open('POST', '/api/upload', true);
170
+
171
+ uploadRequest.upload.addEventListener('progress', (e) => {
172
+ if (e.lengthComputable) {
173
+ const percentComplete = (e.loaded / e.total) * 100;
174
+ progressBar.style.width = percentComplete + '%';
175
+ uploadPercent.innerText = 'Progress : ' + percentComplete.toFixed(2) + '%';
176
+ }
177
+ });
178
+
179
+ uploadRequest.upload.addEventListener('load', async () => {
180
+ await updateSaveProgress(id)
181
+ });
182
+
183
+ uploadRequest.upload.addEventListener('error', () => {
184
+ showToast('❌ Upload failed', 'error');
185
+ setTimeout(() => window.location.reload(), 1000);
186
+ });
187
+
188
+ uploadRequest.send(formData);
189
+ });
190
+
191
+ cancelButton.addEventListener('click', () => {
192
+ if (uploadStep === 1) {
193
+ uploadRequest.abort();
194
+ } else if (uploadStep === 2) {
195
+ const data = { 'id': uploadID }
196
+ postJson('/api/cancelUpload', data)
197
+ }
198
+ showToast('Upload cancelled', 'info');
199
+ setTimeout(() => window.location.reload(), 1000);
200
+ });
201
+
202
+ async function updateSaveProgress(id) {
203
+ console.log('save progress')
204
+ progressBar.style.width = '0%';
205
+ uploadPercent.innerText = 'Progress : 0%'
206
+ document.getElementById('upload-status').innerText = 'Status: Processing File On Backend Server';
207
+
208
+ const interval = setInterval(async () => {
209
+ const response = await postJson('/api/getSaveProgress', { 'id': id })
210
+ const data = response['data']
211
+
212
+ if (data[0] === 'running') {
213
+ const current = data[1];
214
+ const total = data[2];
215
+ document.getElementById('upload-filesize').innerText = 'Filesize: ' + (total / (1024 * 1024)).toFixed(2) + ' MB';
216
+
217
+ const percentComplete = (current / total) * 100;
218
+ progressBar.style.width = percentComplete + '%';
219
+ uploadPercent.innerText = 'Progress : ' + percentComplete.toFixed(2) + '%';
220
+ }
221
+ else if (data[0] === 'completed') {
222
+ clearInterval(interval);
223
+ uploadPercent.innerText = 'Progress : 100%'
224
+ progressBar.style.width = '100%';
225
+
226
+ await handleUpload2(id)
227
+ }
228
+ }, 3000)
229
+
230
+ }
231
+
232
+ async function handleUpload2(id) {
233
+ console.log(id)
234
+ document.getElementById('upload-status').innerText = 'Status: Uploading To Telegram Server';
235
+ progressBar.style.width = '0%';
236
+ uploadPercent.innerText = 'Progress : 0%';
237
+
238
+ const interval = setInterval(async () => {
239
+ const response = await postJson('/api/getUploadProgress', { 'id': id })
240
+ const data = response['data']
241
+
242
+ if (data[0] === 'running') {
243
+ const current = data[1];
244
+ const total = data[2];
245
+ document.getElementById('upload-filesize').innerText = 'Filesize: ' + (total / (1024 * 1024)).toFixed(2) + ' MB';
246
+
247
+ let percentComplete
248
+ if (total === 0) {
249
+ percentComplete = 0
250
+ }
251
+ else {
252
+ percentComplete = (current / total) * 100;
253
+ }
254
+ progressBar.style.width = percentComplete + '%';
255
+ uploadPercent.innerText = 'Progress : ' + percentComplete.toFixed(2) + '%';
256
+ }
257
+ else if (data[0] === 'completed') {
258
+ clearInterval(interval);
259
+ showToast('✨ Upload completed successfully!', 'success');
260
+ setTimeout(() => window.location.reload(), 1000);
261
+ }
262
+ }, 3000)
263
+ }
264
+
265
+ // File Uploader End
266
+
267
+
268
+ // URL Uploader Start
269
+
270
+ async function get_file_info_from_url(url) {
271
+ const data = { 'url': url }
272
+ const json = await postJson('/api/getFileInfoFromUrl', data)
273
+ if (json.status === 'ok') {
274
+ return json.data
275
+ } else {
276
+ throw new Error(`Error Getting File Info : ${json.status}`)
277
+ }
278
+
279
+ }
280
+
281
+ async function start_file_download_from_url(url, filename, singleThreaded) {
282
+ const data = { 'url': url, 'path': getCurrentPath(), 'filename': filename, 'singleThreaded': singleThreaded }
283
+ const json = await postJson('/api/startFileDownloadFromUrl', data)
284
+ if (json.status === 'ok') {
285
+ return json.id
286
+ } else {
287
+ throw new Error(`Error Starting File Download : ${json.status}`)
288
+ }
289
+ }
290
+
291
+ async function download_progress_updater(id, file_name, file_size) {
292
+ uploadID = id;
293
+ uploadStep = 2
294
+ // Showing file uploader
295
+ document.getElementById('bg-blur').style.zIndex = '2';
296
+ document.getElementById('bg-blur').style.opacity = '0.1';
297
+ document.getElementById('file-uploader').style.zIndex = '3';
298
+ document.getElementById('file-uploader').style.opacity = '1';
299
+
300
+ document.getElementById('upload-filename').innerText = 'Filename: ' + file_name;
301
+ document.getElementById('upload-filesize').innerText = 'Filesize: ' + (file_size / (1024 * 1024)).toFixed(2) + ' MB';
302
+
303
+ const interval = setInterval(async () => {
304
+ const response = await postJson('/api/getFileDownloadProgress', { 'id': id })
305
+ const data = response['data']
306
+
307
+ if (data[0] === 'error') {
308
+ clearInterval(interval);
309
+ showToast('❌ Failed to download file from URL', 'error');
310
+ setTimeout(() => window.location.reload(), 1000);
311
+ }
312
+ else if (data[0] === 'completed') {
313
+ clearInterval(interval);
314
+ uploadPercent.innerText = 'Progress : 100%'
315
+ progressBar.style.width = '100%';
316
+ await handleUpload2(id)
317
+ }
318
+ else {
319
+ const current = data[1];
320
+ const total = data[2];
321
+
322
+ const percentComplete = (current / total) * 100;
323
+ progressBar.style.width = percentComplete + '%';
324
+ uploadPercent.innerText = 'Progress : ' + percentComplete.toFixed(2) + '%';
325
+
326
+ if (data[0] === 'Downloading') {
327
+ document.getElementById('upload-status').innerText = 'Status: Downloading File From Url To Backend Server';
328
+ }
329
+ else {
330
+ document.getElementById('upload-status').innerText = `Status: ${data[0]}`;
331
+ }
332
+ }
333
+ }, 3000)
334
+ }
335
+
336
+
337
+ async function Start_URL_Upload() {
338
+ try {
339
+ document.getElementById('new-url-upload').style.opacity = '0';
340
+ setTimeout(() => {
341
+ document.getElementById('new-url-upload').style.zIndex = '-1';
342
+ }, 300)
343
+
344
+ const file_url = document.getElementById('remote-url').value
345
+ const singleThreaded = document.getElementById('single-threaded-toggle').checked
346
+
347
+ const file_info = await get_file_info_from_url(file_url)
348
+ const file_name = file_info.file_name
349
+ const file_size = file_info.file_size
350
+
351
+ if (file_size > MAX_FILE_SIZE) {
352
+ throw new Error(`File size exceeds ${(MAX_FILE_SIZE / (1024 * 1024 * 1024)).toFixed(2)} GB limit`)
353
+ }
354
+
355
+ const id = await start_file_download_from_url(file_url, file_name, singleThreaded)
356
+
357
+ await download_progress_updater(id, file_name, file_size)
358
+
359
+ }
360
+ catch (err) {
361
+ showToast(`❌ ${err.message}`, 'error');
362
+ setTimeout(() => window.location.reload(), 1500);
363
+ }
364
+
365
+
366
+ }
367
+
368
+ // URL Uploader End
website/static/js/extra.js ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function getCurrentPath() {
2
+ const url = new URL(window.location.href);
3
+ const path = url.searchParams.get('path')
4
+ if (path === null) {
5
+ window.location.href = '/?path=/'
6
+ return 'redirect'
7
+ }
8
+ return path
9
+ }
10
+
11
+ function getFolderAuthFromPath() {
12
+ const url = new URL(window.location.href);
13
+ const auth = url.searchParams.get('auth')
14
+ return auth
15
+ }
16
+
17
+ // Changing sidebar section class
18
+ if (getCurrentPath() !== '/') {
19
+ const sections = document.querySelector('.sidebar-menu').getElementsByTagName('a')
20
+ sections[0].setAttribute('class', 'unselected-item')
21
+
22
+ if (getCurrentPath().includes('/trash')) {
23
+ sections[1].setAttribute('class', 'selected-item')
24
+ }
25
+ }
26
+
27
+ function convertBytes(bytes) {
28
+ const kilobyte = 1024;
29
+ const megabyte = kilobyte * 1024;
30
+ const gigabyte = megabyte * 1024;
31
+
32
+ if (bytes >= gigabyte) {
33
+ return (bytes / gigabyte).toFixed(2) + ' GB';
34
+ } else if (bytes >= megabyte) {
35
+ return (bytes / megabyte).toFixed(2) + ' MB';
36
+ } else if (bytes >= kilobyte) {
37
+ return (bytes / kilobyte).toFixed(2) + ' KB';
38
+ } else {
39
+ return bytes + ' bytes';
40
+ }
41
+ }
42
+
43
+ const INPUTS = {}
44
+
45
+ function validateInput(event) {
46
+ console.log('Validating Input')
47
+ const pattern = /^[a-zA-Z0-9 \-_\\[\]()@#!$%*+={}:;<>,.?/|\\~`]*$/;;
48
+ const input = event.target;
49
+ if (!pattern.test(input.value)) {
50
+ input.value = INPUTS[input.id]
51
+ } else {
52
+ INPUTS[input.id] = input.value
53
+ }
54
+ }
55
+
56
+ function getRootUrl() {
57
+ const url = new URL(window.location.href);
58
+ const protocol = url.protocol; // Get the protocol, e.g., "https:"
59
+ const hostname = url.hostname; // Get the hostname, e.g., "sub.example.com" or "192.168.1.1"
60
+ const port = url.port; // Get the port, e.g., "8080"
61
+
62
+ const rootUrl = `${protocol}//${hostname}${port ? ':' + port : ''}`;
63
+
64
+ return rootUrl;
65
+ }
66
+
67
+ function copyTextToClipboard(text) {
68
+ if (navigator.clipboard && navigator.clipboard.writeText) {
69
+ navigator.clipboard.writeText(text).then(function () {
70
+ showToast('🔗 Link copied to clipboard!', 'success');
71
+ }).catch(function (err) {
72
+ console.error('Could not copy text: ', err);
73
+ fallbackCopyTextToClipboard(text);
74
+ });
75
+ } else {
76
+ fallbackCopyTextToClipboard(text);
77
+ }
78
+ }
79
+
80
+ function fallbackCopyTextToClipboard(text) {
81
+ const textArea = document.createElement('textarea');
82
+ textArea.value = text;
83
+ document.body.appendChild(textArea);
84
+ textArea.focus();
85
+ textArea.select();
86
+
87
+ try {
88
+ const successful = document.execCommand('copy');
89
+ if (successful) {
90
+ showToast('🔗 Link copied to clipboard!', 'success');
91
+ } else {
92
+ showToast('Failed to copy the link', 'error');
93
+ }
94
+ } catch (err) {
95
+ console.error('Fallback: Oops, unable to copy', err);
96
+ }
97
+
98
+ document.body.removeChild(textArea);
99
+ }
100
+
101
+ function getPassword() {
102
+ return localStorage.getItem('password')
103
+ }
104
+
105
+ function getRandomId() {
106
+ const length = 6;
107
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
108
+ let result = '';
109
+ for (let i = 0; i < length; i++) {
110
+ result += characters.charAt(Math.floor(Math.random() * characters.length));
111
+ }
112
+ return result;
113
+ }
114
+
115
+ function removeSlash(text) {
116
+ let charactersToRemove = "[/]+"; // Define the characters to remove inside square brackets
117
+ let trimmedStr = text.replace(new RegExp(`^${charactersToRemove}|${charactersToRemove}$`, 'g'), '');
118
+ return trimmedStr;
119
+ }
website/static/js/fileClickHandler.js ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function openFolder() {
2
+ let path = (getCurrentPath() + '/' + this.getAttribute('data-id') + '/').replaceAll('//', '/')
3
+
4
+ const auth = getFolderAuthFromPath()
5
+ if (auth) {
6
+ path = path + '&auth=' + auth
7
+ }
8
+ window.location.href = `/?path=${path}`
9
+ }
10
+
11
+ function openFile() {
12
+ const fileName = this.getAttribute('data-name').toLowerCase()
13
+ let path = '/file?path=' + this.getAttribute('data-path') + '/' + this.getAttribute('data-id')
14
+
15
+ if (fileName.endsWith('.mp4') || fileName.endsWith('.mkv') || fileName.endsWith('.webm') || fileName.endsWith('.mov') || fileName.endsWith('.avi') || fileName.endsWith('.ts') || fileName.endsWith('.ogv')) {
16
+ path = '/stream?url=' + getRootUrl() + path
17
+ }
18
+
19
+ window.open(path, '_blank')
20
+ }
21
+
22
+
23
+ // File More Button Handler Start
24
+
25
+ function openMoreButton(div) {
26
+ const id = div.getAttribute('data-id')
27
+ const moreDiv = document.getElementById(`more-option-${id}`)
28
+
29
+ const rect = div.getBoundingClientRect();
30
+ const x = rect.left + window.scrollX - 40;
31
+ const y = rect.top + window.scrollY;
32
+
33
+ moreDiv.style.zIndex = 2
34
+ moreDiv.style.opacity = 1
35
+ moreDiv.style.left = `${x}px`
36
+ moreDiv.style.top = `${y}px`
37
+
38
+ const isTrash = getCurrentPath().includes('/trash')
39
+
40
+ moreDiv.querySelector('.more-options-focus').focus()
41
+ moreDiv.querySelector('.more-options-focus').addEventListener('blur', closeMoreBtnFocus);
42
+ moreDiv.querySelector('.more-options-focus').addEventListener('focusout', closeMoreBtnFocus);
43
+ if (!isTrash) {
44
+ moreDiv.querySelector(`#rename-${id}`).addEventListener('click', renameFileFolder)
45
+ moreDiv.querySelector(`#trash-${id}`).addEventListener('click', trashFileFolder)
46
+ try {
47
+ moreDiv.querySelector(`#share-${id}`).addEventListener('click', shareFile)
48
+ }
49
+ catch { }
50
+ try {
51
+ moreDiv.querySelector(`#folder-share-${id}`).addEventListener('click', shareFolder)
52
+ }
53
+ catch { }
54
+ }
55
+ else {
56
+ moreDiv.querySelector(`#restore-${id}`).addEventListener('click', restoreFileFolder)
57
+ moreDiv.querySelector(`#delete-${id}`).addEventListener('click', deleteFileFolder)
58
+ }
59
+ }
60
+
61
+ function closeMoreBtnFocus() {
62
+ const moreDiv = this.parentElement
63
+ moreDiv.style.opacity = '0'
64
+ setTimeout(() => {
65
+ moreDiv.style.zIndex = '-1'
66
+ }, 300)
67
+ }
68
+
69
+ // Rename File Folder Start
70
+ function renameFileFolder() {
71
+ const id = this.getAttribute('id').split('-')[1]
72
+ console.log(id)
73
+
74
+ document.getElementById('rename-name').value = this.parentElement.getAttribute('data-name');
75
+ document.getElementById('bg-blur').style.zIndex = '2';
76
+ document.getElementById('bg-blur').style.opacity = '0.1';
77
+
78
+ document.getElementById('rename-file-folder').style.zIndex = '3';
79
+ document.getElementById('rename-file-folder').style.opacity = '1';
80
+ document.getElementById('rename-file-folder').setAttribute('data-id', id);
81
+ setTimeout(() => {
82
+ document.getElementById('rename-name').focus();
83
+ }, 300)
84
+ }
85
+
86
+ document.getElementById('rename-cancel').addEventListener('click', () => {
87
+ document.getElementById('rename-name').value = '';
88
+ document.getElementById('bg-blur').style.opacity = '0';
89
+ setTimeout(() => {
90
+ document.getElementById('bg-blur').style.zIndex = '-1';
91
+ }, 300)
92
+ document.getElementById('rename-file-folder').style.opacity = '0';
93
+ setTimeout(() => {
94
+ document.getElementById('rename-file-folder').style.zIndex = '-1';
95
+ }, 300)
96
+ });
97
+
98
+ document.getElementById('rename-create').addEventListener('click', async () => {
99
+ const name = document.getElementById('rename-name').value;
100
+ if (name === '') {
101
+ showToast('Name cannot be empty', 'error');
102
+ return
103
+ }
104
+
105
+ showLoading('Renaming...');
106
+ const id = document.getElementById('rename-file-folder').getAttribute('data-id')
107
+
108
+ const path = document.getElementById(`more-option-${id}`).getAttribute('data-path') + '/' + id
109
+
110
+ const data = {
111
+ 'name': name,
112
+ 'path': path
113
+ }
114
+
115
+ const response = await postJson('/api/renameFileFolder', data)
116
+ hideLoading();
117
+ if (response.status === 'ok') {
118
+ showToast('✨ Renamed successfully!', 'success');
119
+ setTimeout(() => window.location.reload(), 1000);
120
+ } else {
121
+ showToast('Failed to rename', 'error');
122
+ setTimeout(() => window.location.reload(), 1000);
123
+ }
124
+ });
125
+
126
+
127
+ // Rename File Folder End
128
+
129
+ async function trashFileFolder() {
130
+ const id = this.getAttribute('id').split('-')[1]
131
+ console.log(id)
132
+ const path = document.getElementById(`more-option-${id}`).getAttribute('data-path') + '/' + id
133
+ const data = {
134
+ 'path': path,
135
+ 'trash': true
136
+ }
137
+ showLoading('Moving to trash...');
138
+ const response = await postJson('/api/trashFileFolder', data)
139
+ hideLoading();
140
+
141
+ if (response.status === 'ok') {
142
+ showToast('🗑️ Moved to trash', 'success');
143
+ setTimeout(() => window.location.reload(), 1000);
144
+ } else {
145
+ showToast('Failed to move to trash', 'error');
146
+ setTimeout(() => window.location.reload(), 1000);
147
+ }
148
+ }
149
+
150
+ async function restoreFileFolder() {
151
+ const id = this.getAttribute('id').split('-')[1]
152
+ const path = this.getAttribute('data-path') + '/' + id
153
+ const data = {
154
+ 'path': path,
155
+ 'trash': false
156
+ }
157
+ showLoading('Restoring...');
158
+ const response = await postJson('/api/trashFileFolder', data)
159
+ hideLoading();
160
+
161
+ if (response.status === 'ok') {
162
+ showToast('✅ Restored successfully', 'success');
163
+ setTimeout(() => window.location.reload(), 1000);
164
+ } else {
165
+ showToast('Failed to restore', 'error');
166
+ setTimeout(() => window.location.reload(), 1000);
167
+ }
168
+ }
169
+
170
+ async function deleteFileFolder() {
171
+ const id = this.getAttribute('id').split('-')[1]
172
+ const path = this.getAttribute('data-path') + '/' + id
173
+ const data = {
174
+ 'path': path
175
+ }
176
+ showLoading('Deleting permanently...');
177
+ const response = await postJson('/api/deleteFileFolder', data)
178
+ hideLoading();
179
+
180
+ if (response.status === 'ok') {
181
+ showToast('🗑️ Deleted permanently', 'success');
182
+ setTimeout(() => window.location.reload(), 1000);
183
+ } else {
184
+ showToast('Failed to delete', 'error');
185
+ setTimeout(() => window.location.reload(), 1000);
186
+ }
187
+ }
188
+
189
+ async function shareFile() {
190
+ const fileName = this.parentElement.getAttribute('data-name').toLowerCase()
191
+ const id = this.getAttribute('id').split('-')[1]
192
+ const path = document.getElementById(`more-option-${id}`).getAttribute('data-path') + '/' + id
193
+ const root_url = getRootUrl()
194
+
195
+ let link
196
+ if (fileName.endsWith('.mp4') || fileName.endsWith('.mkv') || fileName.endsWith('.webm') || fileName.endsWith('.mov') || fileName.endsWith('.avi') || fileName.endsWith('.ts') || fileName.endsWith('.ogv')) {
197
+ link = `${root_url}/stream?url=${root_url}/file?path=${path}`
198
+ } else {
199
+ link = `${root_url}/file?path=${path}`
200
+
201
+ }
202
+
203
+ copyTextToClipboard(link)
204
+ }
205
+
206
+
207
+ async function shareFolder() {
208
+ const id = this.getAttribute('id').split('-')[2]
209
+ console.log(id)
210
+ let path = document.getElementById(`more-option-${id}`).getAttribute('data-path') + '/' + id
211
+ const root_url = getRootUrl()
212
+
213
+ const auth = await getFolderShareAuth(path)
214
+ path = path.slice(1)
215
+
216
+ let link = `${root_url}/?path=/share_${path}&auth=${auth}`
217
+ console.log(link)
218
+
219
+ copyTextToClipboard(link)
220
+ }
221
+
222
+ // File More Button Handler End
website/static/js/main.js ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Function removed - now handled by sorting.js
2
+
3
+ document.getElementById('search-form').addEventListener('submit', async (event) => {
4
+ event.preventDefault();
5
+ const query = document.getElementById('file-search').value;
6
+ console.log(query)
7
+ if (query === '') {
8
+ showToast('Search field is empty', 'warning');
9
+ return;
10
+ }
11
+ const path = '/?path=/search_' + encodeURI(query);
12
+ console.log(path)
13
+ window.location = path;
14
+ });
15
+
16
+ // Loading Main Page
17
+
18
+ document.addEventListener('DOMContentLoaded', function () {
19
+ const inputs = ['new-folder-name', 'rename-name', 'file-search']
20
+ for (let i = 0; i < inputs.length; i++) {
21
+ document.getElementById(inputs[i]).addEventListener('input', validateInput);
22
+ }
23
+
24
+ if (getCurrentPath().includes('/share_')) {
25
+ getCurrentDirectory()
26
+ } else {
27
+ if (getPassword() === null) {
28
+ document.getElementById('bg-blur').style.zIndex = '2';
29
+ document.getElementById('bg-blur').style.opacity = '0.1';
30
+
31
+ document.getElementById('get-password').style.zIndex = '3';
32
+ document.getElementById('get-password').style.opacity = '1';
33
+ } else {
34
+ getCurrentDirectory()
35
+ }
36
+ }
37
+
38
+ // Theme Toggle Logic
39
+ const themeBtn = document.getElementById('theme-toggle-btn');
40
+ const themeIcon = document.getElementById('theme-icon');
41
+ const body = document.body;
42
+
43
+ // Check local storage
44
+ if (localStorage.getItem('theme') === 'dark') {
45
+ body.classList.add('dark-mode');
46
+ updateIcon(true);
47
+ }
48
+
49
+ if (themeBtn) {
50
+ themeBtn.addEventListener('click', () => {
51
+ body.classList.toggle('dark-mode');
52
+ const isDark = body.classList.contains('dark-mode');
53
+ localStorage.setItem('theme', isDark ? 'dark' : 'light');
54
+ updateIcon(isDark);
55
+ });
56
+ }
57
+
58
+ function updateIcon(isDark) {
59
+ if (!themeIcon) return;
60
+ if (isDark) {
61
+ // Sun Icon (Material Design Wb_sunny 24px)
62
+ themeIcon.setAttribute("viewBox", "0 0 24 24");
63
+ themeIcon.innerHTML = '<path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/>';
64
+ } else {
65
+ // Moon Icon (Original 24px)
66
+ themeIcon.setAttribute("viewBox", "0 0 24 24");
67
+ themeIcon.innerHTML = '<path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-.46-.04-.92-.1-1.36-.98 1.37-2.58 2.26-4.4 2.26-2.98 0-5.4-2.42-5.4-5.4 0-1.81.89-3.42 2.26-4.4-.44-.06-.9-.1-1.36-.1z"/>';
68
+ }
69
+ }
70
+
71
+
72
+ // Sidebar Toggle Logic
73
+ const sidebarBtn = document.getElementById('sidebar-toggle-btn');
74
+ const container = document.querySelector('.container');
75
+ const sidebar = document.querySelector('.sidebar');
76
+
77
+ // Check local storage for sidebar state
78
+ if (localStorage.getItem('sidebar') === 'collapsed') {
79
+ container.classList.add('sidebar-collapsed');
80
+ }
81
+
82
+ if (sidebarBtn) {
83
+ sidebarBtn.addEventListener('click', () => {
84
+ // On mobile, toggle sidebar visibility
85
+ if (window.innerWidth <= 768) {
86
+ sidebar.classList.toggle('mobile-open');
87
+ } else {
88
+ // On desktop, use collapse
89
+ container.classList.toggle('sidebar-collapsed');
90
+ const isCollapsed = container.classList.contains('sidebar-collapsed');
91
+ localStorage.setItem('sidebar', isCollapsed ? 'collapsed' : 'expanded');
92
+ }
93
+ });
94
+ }
95
+
96
+ // Close mobile sidebar when clicking outside
97
+ document.addEventListener('click', (e) => {
98
+ if (window.innerWidth <= 768) {
99
+ if (!sidebar.contains(e.target) && !sidebarBtn.contains(e.target)) {
100
+ sidebar.classList.remove('mobile-open');
101
+ }
102
+ }
103
+ });
104
+
105
+ // Request Movie Modal Logic
106
+ const requestBtn = document.getElementById('request-movie-btn');
107
+ const requestModal = document.getElementById('request-movie');
108
+ const requestCancel = document.getElementById('request-movie-cancel');
109
+ const requestFetch = document.getElementById('request-movie-fetch');
110
+ const requestSend = document.getElementById('request-movie-send');
111
+ const requestTitleInput = document.getElementById('request-movie-title');
112
+ const requestYearInput = document.getElementById('request-movie-year');
113
+ const requestInfoDiv = document.getElementById('request-movie-info');
114
+
115
+ function openRequestModal() {
116
+ document.getElementById('bg-blur').style.zIndex = '2';
117
+ document.getElementById('bg-blur').style.opacity = '0.1';
118
+ requestModal.style.zIndex = '3';
119
+ requestModal.style.opacity = '1';
120
+ requestTitleInput.focus();
121
+ }
122
+
123
+ function closeRequestModal() {
124
+ requestTitleInput.value = '';
125
+ requestYearInput.value = '';
126
+ requestInfoDiv.innerHTML = '';
127
+ document.getElementById('bg-blur').style.opacity = '0';
128
+ setTimeout(() => {
129
+ document.getElementById('bg-blur').style.zIndex = '-1';
130
+ }, 300);
131
+ requestModal.style.opacity = '0';
132
+ setTimeout(() => {
133
+ requestModal.style.zIndex = '-1';
134
+ }, 300);
135
+ }
136
+
137
+ if (requestBtn) {
138
+ requestBtn.addEventListener('click', openRequestModal);
139
+ }
140
+
141
+ if (requestCancel) {
142
+ requestCancel.addEventListener('click', closeRequestModal);
143
+ }
144
+
145
+ if (requestFetch) {
146
+ requestFetch.addEventListener('click', async () => {
147
+ const title = requestTitleInput.value.trim();
148
+ const year = requestYearInput.value.trim();
149
+ if (!title) {
150
+ showToast('Movie title is required', 'warning');
151
+ return;
152
+ }
153
+ showLoading('Fetching from TMDB...');
154
+ const res = await tmdbSearchMovie(title, year);
155
+ hideLoading();
156
+ if (res.status === 'ok' && res.data) {
157
+ const m = res.data;
158
+ const y = m.year || '';
159
+ requestTitleInput.value = m.title || title;
160
+ if (y) {
161
+ requestYearInput.value = y;
162
+ }
163
+ requestInfoDiv.innerHTML = `
164
+ <strong>${m.title || ''} (${y})</strong><br>
165
+ <small>${m.overview || 'No overview available.'}</small>
166
+ `;
167
+ showToast('TMDB result loaded', 'success');
168
+ } else if (res.status === 'no_results') {
169
+ requestInfoDiv.innerHTML = '<small>No results found on TMDB.</small>';
170
+ showToast('No TMDB results found', 'warning');
171
+ } else if (res.status === 'TMDB not configured') {
172
+ requestInfoDiv.innerHTML = '<small>TMDB API key not configured on server.</small>';
173
+ showToast('TMDB not configured on server', 'error');
174
+ } else {
175
+ requestInfoDiv.innerHTML = `<small>Error: ${res.status}</small>`;
176
+ showToast('Failed to fetch from TMDB', 'error');
177
+ }
178
+ });
179
+ }
180
+
181
+ if (requestSend) {
182
+ requestSend.addEventListener('click', async () => {
183
+ const title = requestTitleInput.value.trim();
184
+ const year = requestYearInput.value.trim();
185
+ if (!title || !year) {
186
+ showToast('Title and year are required', 'warning');
187
+ return;
188
+ }
189
+ showLoading('Requesting movie...');
190
+ const res = await requestMovie(title, year);
191
+ hideLoading();
192
+
193
+ if (res.status === 'available' && res.file) {
194
+ const root = getRootUrl();
195
+ const path = res.file.path;
196
+ const isVideo = res.file.is_video;
197
+ let link;
198
+ if (isVideo) {
199
+ link = `${root}/stream?url=${root}/file?path=${path}`;
200
+ } else {
201
+ link = `${root}/file?path=${path}`;
202
+ }
203
+ copyTextToClipboard(link);
204
+ showToast('✅ Movie available. Link copied to clipboard!', 'success');
205
+ closeRequestModal();
206
+ } else if (res.status === 'not_found') {
207
+ showToast('Movie not available yet. Request saved.', 'info');
208
+ closeRequestModal();
209
+ } else if (res.status === 'Invalid password') {
210
+ showToast('Invalid admin password. Please login again.', 'error');
211
+ } else {
212
+ showToast(`Failed to request movie: ${res.status}`, 'error');
213
+ }
214
+ });
215
+ }
216
+ });