Spaces:
Sleeping
Sleeping
Commit ·
19f62c1
0
Parent(s):
Initial commit for beta-tgdrive
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +35 -0
- .gitignore +9 -0
- Dockerfile +18 -0
- LICENSE +21 -0
- README.md +179 -0
- SETUP_SUMMARY.txt +27 -0
- cache/bot.session +0 -0
- cache/main_bot.session +0 -0
- config.py +89 -0
- docker_start.sh +3 -0
- main.py +528 -0
- render.yaml +24 -0
- requirements.txt +13 -0
- runtime.txt +1 -0
- sample.env +20 -0
- start_main.py +3 -0
- utils/bot_mode.py +266 -0
- utils/clients.py +208 -0
- utils/directoryHandler.py +409 -0
- utils/downloader.py +85 -0
- utils/extra.py +146 -0
- utils/logger.py +48 -0
- utils/mongo_indexer.py +121 -0
- utils/movie_requests.py +75 -0
- utils/streamer/__init__.py +82 -0
- utils/streamer/custom_dl.py +223 -0
- utils/streamer/file_properties.py +84 -0
- utils/uploader.py +99 -0
- website/VideoPlayer.html +286 -0
- website/home.html +219 -0
- website/static/assets/file-icon.svg +1 -0
- website/static/assets/folder-icon.svg +1 -0
- website/static/assets/folder-solid-icon.svg +1 -0
- website/static/assets/home-icon.svg +1 -0
- website/static/assets/info-icon-small.svg +1 -0
- website/static/assets/link-icon.svg +1 -0
- website/static/assets/load-icon.svg +1 -0
- website/static/assets/more-icon.svg +1 -0
- website/static/assets/pencil-icon.svg +1 -0
- website/static/assets/plus-icon.svg +1 -0
- website/static/assets/profile-icon.svg +1 -0
- website/static/assets/search-icon.svg +1 -0
- website/static/assets/share-icon.svg +1 -0
- website/static/assets/trash-icon.svg +1 -0
- website/static/assets/upload-icon.svg +1 -0
- website/static/home.css +1112 -0
- website/static/js/apiHandler.js +368 -0
- website/static/js/extra.js +119 -0
- website/static/js/fileClickHandler.js +222 -0
- 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 |
+
[](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 |
+
});
|