This commit is contained in:
2025-04-13 11:59:50 +02:00
parent 80640d2580
commit fe3191d4a2
21 changed files with 567 additions and 117 deletions

View File

@@ -1,6 +1,8 @@
import os import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
django_asgi_app = get_asgi_application()
from channels.auth import AuthMiddlewareStack from channels.auth import AuthMiddlewareStack
from channels.sessions import SessionMiddlewareStack from channels.sessions import SessionMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter from channels.routing import ProtocolTypeRouter, URLRouter
@@ -8,8 +10,7 @@ from channels.routing import ProtocolTypeRouter, URLRouter
from app.channels_middleware import JwtOrSessionAuthMiddleware from app.channels_middleware import JwtOrSessionAuthMiddleware
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
django_asgi_app = get_asgi_application()
from .ws_urls import websocket_urlpatterns from .ws_urls import websocket_urlpatterns

View File

@@ -59,8 +59,6 @@ INSTALLED_APPS = [
'api', 'api',
'torrent', 'torrent',
] ]
if DEBUG:
INSTALLED_APPS = ["daphne"] + INSTALLED_APPS
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',

View File

@@ -3,7 +3,7 @@ from django.http import StreamingHttpResponse
import zlib import zlib
import datetime import datetime
import os import os
import aiofiles import anyio
from stat import S_IFREG from stat import S_IFREG
from stream_zip import ZIP_64, stream_zip, async_stream_zip from stream_zip import ZIP_64, stream_zip, async_stream_zip
from channels.layers import get_channel_layer from channels.layers import get_channel_layer
@@ -61,7 +61,7 @@ class StreamingZipFileResponse(StreamingHttpResponse):
async def contents(path): async def contents(path):
try: try:
async with aiofiles.open(path, "rb") as f: async with await anyio.open_file(path, "rb") as f:
while chunk := await f.read(64 * 1024): while chunk := await f.read(64 * 1024):
yield chunk yield chunk
except RuntimeError as e: except RuntimeError as e:

View File

@@ -1,5 +1,10 @@
#!/bin/bash
CORES=$(nproc)
WORKERS=$((2 * CORES + 1))
cd /app/frontend && yarn dev & cd /app/frontend && yarn dev &
cd /app && python manage.py runserver 0.0.0.0:8000 & cd /app && uvicorn app.asgi:application --workers $WORKERS --host 0.0.0.0 --port 8000 --lifespan off --loop uvloop --ws websockets --reload &
wait -n wait -n

View File

@@ -11,24 +11,26 @@
</v-row> </v-row>
</v-list-item-action> </v-list-item-action>
</v-list-item> </v-list-item>
<v-divider/>
</v-list> </v-list>
<v-divider/>
<UserStats :stats="user_stats"/> <UserStats :stats="user_stats"/>
<v-divider/>
{{dm_status}}
<DM v-if="dm_activated" :dm_files="dm_files" :dm_status="dm_status" :dm_download_location="dm_download_location"/>
</v-navigation-drawer> </v-navigation-drawer>
<v-navigation-drawer v-model="right_drawer" location="right" temporary> <v-navigation-drawer v-model="right_drawer" location="right" temporary>
<Friend v-if="right_drawer" :active_user="display_user.id" @userSelected="userSelectedUpdated"/> <Friend v-if="right_drawer" :active_user="display_user.id" @userSelected="userSelectedUpdated"/>
</v-navigation-drawer> </v-navigation-drawer>
<v-app-bar app> <v-app-bar>
<v-app-bar-title> <v-app-bar-title style="max-width: 150px">
<v-row align="center" justify="start" style="max-width: 100%">
<v-col class="v-col-1 text-center">
<v-btn variant="plain" href="/">Oxpanel</v-btn> <v-btn variant="plain" href="/">Oxpanel</v-btn>
</v-col> </v-app-bar-title>
<v-col class="v-col-3"> <v-row align="center" justify="center" style="max-width: 500px" class="mx-auto">
<v-text-field flat solo-inverted hide-details prepend-inner-icon="mdi-magnify" label="Search" v-model="filters.search" clearable @click:clear="filters.search = ''"/> <v-col>
<v-text-field density="comfortable" flat solo-inverted hide-details prepend-inner-icon="mdi-magnify" label="Search" v-model="filters.search" clearable @click:clear="filters.search = ''"/>
</v-col> </v-col>
</v-row> </v-row>
</v-app-bar-title>
<template v-slot:append> <template v-slot:append>
<v-menu> <v-menu>
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
@@ -67,6 +69,7 @@ import TorrentList from "@/components/torrent/TorrentList.vue";
import UploadForm from "@/components/torrent/UploadForm.vue"; import UploadForm from "@/components/torrent/UploadForm.vue";
import Friend from "@/components/auth/Friend.vue"; import Friend from "@/components/auth/Friend.vue";
import UserStats from "@/components/torrent/UserStats.vue"; import UserStats from "@/components/torrent/UserStats.vue";
import DM from "@/components/torrent/DM.vue";
</script> </script>
<script> <script>
@@ -83,18 +86,23 @@ export default {
filters: {}, filters: {},
torrents: [], torrents: [],
user_stats: { user_stats: {
"torrents_size": 0, torrents_size: 0,
"torrents_len": 0, torrents_len: 0,
"torrent_len_shared": 0, torrent_len_shared: 0,
"torrents_total_len": 0, torrents_total_len: 0,
"user_max_size": 0, user_max_size: 0,
"user_usage_percent": 0, user_usage_percent: 0,
"disk_total": 0, disk_total: 0,
"disk_used": 0, disk_used: 0,
"disk_free": 0, disk_free: 0,
"disk_usage_percent": 0, disk_usage_percent: 0,
}, },
filter_timer: null, filter_timer: null,
dm_files: {},
// dm_stats: {},
dm_status: {},
dm_download_location: "",
dm_activated: false
} }
}, },
watch: { watch: {
@@ -134,6 +142,29 @@ export default {
}) })
await this.fetchTorrents(); await this.fetchTorrents();
await this.fetchUserStats(); await this.fetchUserStats();
if(this.$qt.connect()){
this.$qt.on("ready", () => {
this.dm_status = this.$qt.getProperty("dm_status");
this.dm_files = this.$qt.getProperty("dm_files");
// this.dm_stats = this.$qt.getProperty("dm_stats");
this.dm_download_location = this.$qt.getProperty("dm_download_location");
this.dm_activated = true;
// console.log(JSON.stringify(this.dm_files))
this.$qt.on("status_updated", data => {
this.dm_status = data;
})
// this.$qt.on("stats_updated", data => {
// this.dm_stats = data;
// })
this.$qt.on("files_updated", data => {
this.dm_files = data;
})
this.$qt.on("download_location_updated", data => {
this.dm_download_location = data;
})
})
}
}, },
methods: { methods: {
async fetchUserStats(){ async fetchUserStats(){
@@ -219,3 +250,7 @@ export default {
} }
} }
</script> </script>
<style scoped>
</style>

View File

@@ -0,0 +1,75 @@
<template>
<v-list>
<v-list-item prepend-icon="mdi-details" @click="details_enabled = true" title="Download Manager"/>
<v-dialog width="80%" v-model="details_enabled">
<DMDetails :dm_files="dm_files" :dm_status="dm_status" :dm_download_location="dm_download_location"/>
</v-dialog>
<v-list-item
:prepend-icon="dm_status.pause ? 'mdi-play' : 'mdi-pause'"
@click="$qt.callMethod('set_pause', !dm_status.pause)"
>
<template v-slot:title>
{{ dm_status.pause ? 'Start':'Pause' }}
</template>
</v-list-item>
<v-list-item prepend-icon="mdi-speedometer">
<template v-slot:title>
{{fs_speed_format(dm_status.speed)}}/s
</template>
</v-list-item>
<v-list-item prepend-icon="mdi-file-download" >
<template v-slot:title>
{{dm_status.downloaded_files}}/{{dm_status.total_files}}
</template>
</v-list-item>
<v-list-item prepend-icon="mdi-tape-drive">
<template v-slot:title>
{{fs_format(dm_status.downloaded_size)}}/{{fs_format(dm_status.total_size)}}
</template>
<template v-slot:subtitle>
<v-progress-linear :model-value="percentDownloaded" height="12" color="green">{{percentDownloaded.toFixed(1)}}%</v-progress-linear>
</template>
</v-list-item>
</v-list>
</template>
<script setup>
import DMDetails from "@/components/torrent/DMDetails.vue";
</script>
<script>
import {fs_format, fs_speed_format} from "@/plugins/utils.js";
export default {
methods: {fs_format, fs_speed_format},
props: {
dm_files: {
required: true,
type: Object
},
dm_status: {
required: true,
type: Object
},
dm_download_location: {
required: true,
type: String
}
},
data(){
return {
details_enabled: false,
}
},
computed: {
percentDownloaded(){
return this.dm_status.total_size > 0 ? this.dm_status.downloaded_size / this.dm_status.total_size * 100: 0;
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,123 @@
<template>
<v-card class="dm-details-card">
<v-card-actions>
<v-text-field :model-value="dm_download_location" readonly @click="$qt.callMethod('change_path')"></v-text-field>
</v-card-actions>
<v-divider/>
<v-card-text class="scrollable-content">
<!-- Liste des fichiers en téléchargement -->
<div v-if="Object.keys(filterFiles.downloading).length > 0">
<h3>Fichiers en téléchargement</h3>
<v-list density="compact">
<v-list-item v-for="(file, key) in filterFiles.downloading" :key="key">
<template v-slot:title>
{{ file.rel_path }}
</template>
<template v-slot:subtitle>
</template>
</v-list-item>
</v-list>
</div>
<!-- Liste des fichiers en attente -->
<div>
<h3>Fichiers en attente</h3>
<v-list density="compact">
<v-list-item v-for="(file, key) in filterFiles.waiting" :key="key">
<template v-slot:title>
{{ file.rel_path }}
</template>
<template v-slot:subtitle>
En attente
</template>
</v-list-item>
</v-list>
</div>
<!-- Liste des fichiers terminés -->
<div>
<h3>Fichiers terminés</h3>
<v-list density="compact">
<v-list-item v-for="(file, key) in filterFiles.finished" :key="key">
<template v-slot:title>
{{ file.rel_path }}
</template>
<template v-slot:subtitle>
</template>
</v-list-item>
</v-list>
</div>
</v-card-text>
</v-card>
</template>
<script setup>
import {fs_format, fs_speed_format} from "@/plugins/utils.js";
</script>
<script>
export default {
props: {
dm_files: {
required: true,
type: Object
},
dm_status: {
required: true,
type: Object
},
dm_download_location: {
required: true,
type: String
}
},
data(){
return {
}
},
mounted(){
},
methods: {
},
computed: {
filterFiles(){
let waiting = {},
downloading = {},
finished = {};
for(const [file_id, file] of Object.entries(this.dm_files)){
if(file.downloaded){
finished[file_id] = file;
}else if(file_id in this.dm_status.downloading){
downloading[file_id] = file;
}else{
waiting[file_id] = file;
}
}
return {waiting, downloading, finished};
}
}
};
</script>
<style>
.scrollable-content {
max-height: 400px; /* Définissez la hauteur maximale souhaitée */
overflow-y: auto; /* Active le défilement vertical quand nécessaire */
overflow-x: hidden; /* Empêche le défilement horizontal */
padding-right: 8px; /* Espace pour la barre de défilement */
}
.dm-details-card {
display: flex;
flex-direction: column;
}
</style>

View File

@@ -44,11 +44,16 @@ export default {
methods: { methods: {
downloadClicked(){ downloadClicked(){
if(!this.is_download_finished) return; if(!this.is_download_finished) return;
if(this.$qt.is_active){
this.$qt.callMethod("add_files", this.file);
}else{
let a = document.createElement("a"); let a = document.createElement("a");
a.href = this.file.download_url; a.href = this.file.download_url;
a.setAttribute("download", "download"); a.setAttribute("download", "download");
a.click(); a.click();
} }
}
} }
} }
</script> </script>

View File

@@ -42,6 +42,7 @@ export default {
let response = await fetch(`/api/torrent/files?${new URLSearchParams(this.filters)}`); let response = await fetch(`/api/torrent/files?${new URLSearchParams(this.filters)}`);
this.files = await response.json(); this.files = await response.json();
this.loading = false; this.loading = false;
return this.files;
} }
} }
} }

View File

@@ -2,14 +2,14 @@
<v-card @click="show_files = !show_files"> <v-card @click="show_files = !show_files">
<v-progress-linear height="20" :color="progressData.color" :model-value="progressData.value"> <v-progress-linear height="20" :color="progressData.color" :model-value="progressData.value">
<!-- barre de progression --> <!-- barre de progression -->
<v-progress-circular <!-- <v-progress-circular-->
v-if="!torrent.transmission_data.rateDownload && !torrent.transmission_data.rateUpload" <!-- v-if="!torrent.transmission_data.rateDownload && !torrent.transmission_data.rateUpload"-->
size="15" <!-- size="15"-->
indeterminate <!-- indeterminate-->
:color="torrent.transmission_data.progress < 100 ? 'green':'red'" <!-- :color="torrent.transmission_data.progress < 100 ? 'green':'red'"-->
/> <!-- />-->
<v-icon <v-icon
v-else v-if="torrent.transmission_data.rateDownload && torrent.transmission_data.rateUpload"
:color="torrent.transmission_data.rateDownload ? 'green':'red'" :color="torrent.transmission_data.rateDownload ? 'green':'red'"
:icon="torrent.transmission_data.rateDownload ? 'mdi-arrow-down-bold':'mdi-arrow-up-bold'" :icon="torrent.transmission_data.rateDownload ? 'mdi-arrow-down-bold':'mdi-arrow-up-bold'"
/> />
@@ -52,7 +52,11 @@
<v-expand-transition> <v-expand-transition>
<div v-if="show_files"> <div v-if="show_files">
<v-divider /> <v-divider />
<FileList v-if="show_files" :torrent_id="torrent.id" :is_download_finished="torrent.transmission_data.progress >= 100"/> <FileList
v-if="show_files"
:torrent_id="torrent.id"
:is_download_finished="torrent.transmission_data.progress >= 100"
/>
</div> </div>
</v-expand-transition> </v-expand-transition>
</v-card> </v-card>
@@ -88,10 +92,16 @@ export default {
methods: { methods: {
async downloadClicked(){ async downloadClicked(){
if(this.torrent.transmission_data.progress < 100) return; if(this.torrent.transmission_data.progress < 100) return;
if(this.$qt.is_active){
let response = await fetch(`/api/torrent/files/?torrent=${this.torrent.id}`);
let files = await response.json();
this.$qt.callMethod("add_files", files);
}else{
let a = document.createElement("a"); let a = document.createElement("a");
a.href = this.torrent.download_url; a.href = this.torrent.download_url;
a.setAttribute("download", "download"); a.setAttribute("download", "download");
a.click(); a.click();
}
}, },
async deleteClicked(){ async deleteClicked(){
this.$emit("delete", this.torrent.id); this.$emit("delete", this.torrent.id);

View File

@@ -0,0 +1,13 @@
import { reactive } from "vue";
class Params {
constructor() {
this.dl_hotfix_enabled = false;
}
}
export default {
install: (app, options) => {
app.config.globalProperties.$params = reactive(new Params());
}
}

View File

@@ -0,0 +1,76 @@
import { EventEmitter } from 'events';
class QtWebChannel {
constructor() {
this.handler = null;
this.eventEmitter = new EventEmitter(); // Ajouter l'EventEmitter
}
get is_active(){
return Boolean(this.handler)
}
connect() {
if(typeof QWebChannel !== 'undefined') {
this.handler = new QWebChannel(qt.webChannelTransport, channel => {
channel.objects.handler.on_message.connect((message) => {
this.handleMessage(message);
});
this.callMethod('on_site_ready', args => {
this.eventEmitter.emit('ready');
})
console.log(this.getProperty("dm_status"))
})
return true;
}else{
return false;
}
}
callMethod(function_name, ...args){
console.log("call method", function_name, args)
if(this.handler !== null){
return this.handler.objects.handler[function_name](...args)
}else{
return null;
}
}
getProperty(property_name, default_value){
if(this.handler !== null){
let property = this.handler.objects.handler[property_name];
if(property !== undefined){
return property;
}
}
return default_value;
}
handleMessage(message){
try {
if("context" in message){
this.eventEmitter.emit(message.context, message.content);
}
return message;
} catch (error) {
console.error('Failed to parse message as JSON:', error);
this.eventEmitter.emit('invalid_message', { raw: message, error });
return null;
}
}
on(event, callback) {
this.eventEmitter.on(event, callback);
}
off(event, callback) {
this.eventEmitter.off(event, callback);
}
}
export default {
install: (app, options) => {
app.config.globalProperties.$qt = new QtWebChannel();
}
}

View File

@@ -1,7 +1,8 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
import Vuetify from "./vuetify" import Vuetify from "./vuetify"
import Ws from "./ws"; import Ws from "./ws";
import QtWC from "./qtwebchannel.js"
export function createVue(component, dom_id){ export function createVue(component, dom_id){
return createApp(component).use(Vuetify).use(Ws).mount(dom_id) return createApp(component).use(Vuetify).use(Ws).use(QtWC).mount(dom_id)
} }

View File

@@ -11,7 +11,7 @@ class WebSocketClient {
connect(url) { connect(url) {
this.url = url; this.url = url;
this.socket = new window.WebSocket(this.url); this.socket = new window.WebSocket(this.formatWebSocketUrl(this.url));
this.socket.onopen = () => { this.socket.onopen = () => {
console.log('WebSocket connected'); console.log('WebSocket connected');
@@ -69,8 +69,33 @@ class WebSocketClient {
console.error('WebSocket is not open. Cannot send data.'); console.error('WebSocket is not open. Cannot send data.');
} }
} }
formatWebSocketUrl(url) {
// Vérifier si l'URL est déjà un protocole WebSocket
if (url.startsWith('ws://') || url.startsWith('wss://')) {
return url;
} }
// Déterminer si on utilise le protocole sécurisé (wss) ou non (ws)
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// Si l'URL est relative (/mon_url)
if (url.startsWith('/')) {
return `${protocol}//${window.location.host}${url}`;
}
// Si l'URL est complète (http://monsite.com/mon_url)
if (url.startsWith('http://') || url.startsWith('https://')) {
return url.replace(/^http(s)?:\/\//, (_, s) => s ? 'wss://' : 'ws://');
}
// Si c'est juste un chemin sans slash au début
return `${protocol}//${window.location.host}/${url}`;
}
}
export default { export default {
install: (app, options) => { install: (app, options) => {
app.config.globalProperties.$ws = new WebSocketClient(); app.config.globalProperties.$ws = new WebSocketClient();

View File

@@ -0,0 +1,26 @@
::-webkit-scrollbar {
width: 10px;
height: 10px;
display: block !important;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
display: block !important;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 5px;
display: block !important;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Pour Firefox */
* {
scrollbar-width: thin;
scrollbar-color: #888 #f1f1f1;
}

View File

@@ -4,7 +4,7 @@ django-cors-headers
djangorestframework djangorestframework
django-filter django-filter
djangorestframework-simplejwt djangorestframework-simplejwt
channels[daphne] channels
channels_redis channels_redis
celery celery
@@ -14,4 +14,7 @@ psycopg[binary]
uvicorn uvicorn
transmission-rpc transmission-rpc
stream-zip stream-zip
aiofiles anyio
websockets
uvloop
watchfiles

View File

@@ -73,7 +73,7 @@ class File(models.Model):
def mime_types(self): def mime_types(self):
mime = mimetypes.guess_type(self.pathname) mime = mimetypes.guess_type(self.pathname)
if mime: if mime:
return mime return mime[0] or mime[1] or "application/octet-stream"
else: else:
return "application/octet-stream" return "application/octet-stream"
@@ -87,7 +87,21 @@ class File(models.Model):
@property @property
def accel_redirect(self): def accel_redirect(self):
return shlex.quote(f"{settings.NGINX_ACCEL_BASE}/{self.pathname}") # Encode chaque partie du chemin séparément pour préserver la structure
encoded_parts = []
for part in self.pathname.parts:
# Ignorer un slash initial si présent
if part == '/' or part == '\\':
continue
encoded_parts.append(quote(part))
# Construction du chemin final avec le préfixe Nginx
if settings.NGINX_ACCEL_BASE.endswith('/'):
base = settings.NGINX_ACCEL_BASE.rstrip('/')
else:
base = settings.NGINX_ACCEL_BASE
return f"{base}/{'/'.join(encoded_parts)}"
@property @property
def disposition(self): def disposition(self):

View File

@@ -6,4 +6,14 @@
const current_user = {{ request.user.min_infos|safe }}; const current_user = {{ request.user.min_infos|safe }};
</script> </script>
{% vite_asset "app/torrent.js" %} {% vite_asset "app/torrent.js" %}
<script>
function QWebInit(){
if(typeof(QWebChannel) !== 'undefined'){
new QWebChannel(qt.webChannelTransport, channel => {
})
}
}
</script>
{% endblock %} {% endblock %}

View File

@@ -1,10 +1,11 @@
from django.urls import path from django.urls import path
from .views import HomeView, download_file, download_torrent from .views import HomeView, download_file, download_torrent, pping
app_name = "torrent" app_name = "torrent"
urlpatterns = [ urlpatterns = [
path("", HomeView.as_view(), name="home"), path("", HomeView.as_view(), name="home"),
path("pping/", pping, name="pping"),
path("download_file/<uuid:file_id>", download_file, name="download_file"), path("download_file/<uuid:file_id>", download_file, name="download_file"),
path("download_torrent/<str:torrent_id>", download_torrent, name="download_torrent"), path("download_torrent/<str:torrent_id>", download_torrent, name="download_torrent"),
] ]

View File

@@ -6,11 +6,11 @@ from django.db.models import Q, Count, OuterRef
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.http import HttpResponse, Http404, StreamingHttpResponse from django.http import HttpResponse, Http404, StreamingHttpResponse
import aiofiles
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from rest_framework import mixins from rest_framework import mixins
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.decorators import action from rest_framework.decorators import action
import anyio
from app.utils import StreamingZipFileResponse from app.utils import StreamingZipFileResponse
from user.models import User from user.models import User
@@ -23,6 +23,10 @@ class HomeView(LoginRequiredMixin, TemplateView):
template_name = "torrent/home.html" template_name = "torrent/home.html"
def pping(request):
return HttpResponse(str(dict(request.session)))
async def download_file(request, file_id): async def download_file(request, file_id):
user = await request.auser() user = await request.auser()
qs = File.objects.filter( qs = File.objects.filter(
@@ -32,16 +36,16 @@ async def download_file(request, file_id):
| Q(torrent__shared_users__friends=user), | Q(torrent__shared_users__friends=user),
torrent__transmission_data__progress__gte=100, torrent__transmission_data__progress__gte=100,
pk=file_id pk=file_id
) ).distinct()
try: try:
file = await qs.aget() file = await qs.aget()
except File.DoesNotExist: except File.DoesNotExist:
raise Http404() raise Http404()
else: else:
if request.GET.get("dl_hotfix", "0") == "1": if int(request.GET.get("dl_hotfix", 0)) == 1:
async def read_file(): async def read_file():
async with aiofiles.open(file.abs_pathname, "rb") as f: async with await anyio.open_file(file.abs_pathname, "rb") as f:
while chunk := await f.read(128 * 1024): while chunk := await f.read(128 * 1024):
yield chunk yield chunk
response = StreamingHttpResponse(read_file()) response = StreamingHttpResponse(read_file())
@@ -57,6 +61,30 @@ async def download_file(request, file_id):
return response return response
async def flux_file(request, file_id):
user = await request.auser()
qs = File.objects.filter(
Q(torrent__user=user)
| Q(torrent__shared_users=user)
| Q(torrent__user__friends=user)
| Q(torrent__shared_users__friends=user),
torrent__transmission_data__progress__gte=100,
pk=file_id
).distinct()
try:
file = await qs.aget()
except File.DoesNotExist:
raise Http404()
else:
response = HttpResponse()
response["X-Accel-Redirect"] = file.accel_redirect
response["X-Accel-Buffering"] = "no"
response["Content-Type"] = file.mime_types
response["Content-Disposition"] = file.disposition
return response
async def download_torrent(request, torrent_id): async def download_torrent(request, torrent_id):
# py version # py version
user = await request.auser() user = await request.auser()
@@ -67,11 +95,11 @@ async def download_torrent(request, torrent_id):
| Q(shared_users__friends=user), | Q(shared_users__friends=user),
transmission_data__progress__gte=100, transmission_data__progress__gte=100,
pk=torrent_id pk=torrent_id
).annotate(count_files=Count("files")) ).annotate(count_files=Count("files")).distinct()
torrent = await qs.aget() torrent = await qs.aget()
if torrent.count_files == 1: if await torrent.alen_files == 1:
file = await torrent.files.afirst() file = await torrent.files.afirst()
return redirect(reverse("torrent:download_file", kwargs={ return redirect(reverse("torrent:download_file", kwargs={
"file_id": file.pk "file_id": file.pk

View File

@@ -52,7 +52,7 @@ services:
&& cd frontend && yarn build && cd .. && cd frontend && yarn build && cd ..
&& python manage.py collectstatic --noinput && python manage.py collectstatic --noinput
&& python manage.py migrate && python manage.py migrate
&& uvicorn app.asgi:application --workers 3 --host 0.0.0.0 --port 8000 --lifespan off --loop asyncio --ws websockets" && uvicorn app.asgi:application --workers 3 --host 0.0.0.0 --port 8000 --lifespan off --loop uvloop --ws websockets"
# celery: # celery:
# extends: # extends: