init
This commit is contained in:
0
app/torrent/__init__.py
Normal file
0
app/torrent/__init__.py
Normal file
13
app/torrent/admin.py
Normal file
13
app/torrent/admin.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django.contrib import admin
|
||||
from django.template.defaultfilters import filesizeformat
|
||||
|
||||
from .models import Torrent
|
||||
|
||||
|
||||
@admin.register(Torrent)
|
||||
class TorrentAdmin(admin.ModelAdmin):
|
||||
list_display = ["name", "user", "display_size", "len_files"]
|
||||
list_filter = ["user"]
|
||||
|
||||
def display_size(self, obj):
|
||||
return filesizeformat(obj.size)
|
||||
15
app/torrent/apps.py
Normal file
15
app/torrent/apps.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TorrentConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'torrent'
|
||||
|
||||
def ready(self):
|
||||
from django.db.models.signals import post_save, pre_delete, m2m_changed
|
||||
from .signals import on_post_save_torrent, on_pre_delete_torrent, on_shared_user_changed
|
||||
from .models import Torrent
|
||||
|
||||
post_save.connect(on_post_save_torrent, sender=Torrent)
|
||||
pre_delete.connect(on_pre_delete_torrent, sender=Torrent)
|
||||
m2m_changed.connect(on_shared_user_changed, sender=Torrent.shared_users.through)
|
||||
100
app/torrent/consumers.py
Normal file
100
app/torrent/consumers.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from django.db.models import Q
|
||||
from django.db.models.functions import Coalesce
|
||||
|
||||
from channels.generic.websocket import AsyncJsonWebsocketConsumer
|
||||
from typing import Optional, Union
|
||||
import asyncio
|
||||
|
||||
from user.models import User
|
||||
from .models import Torrent
|
||||
|
||||
|
||||
class TorrentEventConsumer(AsyncJsonWebsocketConsumer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.channel_groups = set()
|
||||
self.user: Optional[User] = None
|
||||
self.follow_user: Optional[User] = None
|
||||
|
||||
async def connect(self):
|
||||
self.user = self.scope['user']
|
||||
if not self.user.is_authenticated:
|
||||
await self.close()
|
||||
return
|
||||
|
||||
self.follow_user = self.user
|
||||
await self.channel_layer.group_add(f"user_{self.follow_user.id}", self.channel_name)
|
||||
|
||||
# user_id = int(self.scope['url_route']["kwargs"]["user_id"])
|
||||
# if user_id == self.user.id:
|
||||
# self.follow_user = self.user
|
||||
# else:
|
||||
# if await self.change_follow_user(user_id) is None:
|
||||
# await self.close()
|
||||
# return
|
||||
|
||||
await self.channel_layer.group_add("torrent", self.channel_name)
|
||||
await self.accept()
|
||||
|
||||
async def disconnect(self, code):
|
||||
await self.channel_layer.group_discard("torrent", self.channel_name)
|
||||
|
||||
async def dispatch(self, message):
|
||||
print("dispatch ws :", message)
|
||||
return await super().dispatch(message)
|
||||
|
||||
async def receive_json(self, content, **kwargs):
|
||||
if "context" not in content:
|
||||
return
|
||||
match content["context"]:
|
||||
case "change_follow_user":
|
||||
await self.change_follow_user(content["user_id"])
|
||||
case _:
|
||||
print("call websocket not supported", content)
|
||||
|
||||
async def change_follow_user(self, user_id):
|
||||
await self.channel_layer.group_discard(f"user_{self.follow_user.id}", self.channel_name)
|
||||
if user_id == self.user.id:
|
||||
self.follow_user = self.user
|
||||
await self.channel_layer.group_add(f"user_{self.follow_user.id}", self.channel_name)
|
||||
return self.follow_user
|
||||
elif await self.user.friends.filter(id=user_id).aexists():
|
||||
self.follow_user = await User.objects.filter(id=user_id).aget()
|
||||
await self.channel_layer.group_add(f"user_{self.follow_user.id}", self.channel_name)
|
||||
return self.follow_user
|
||||
else:
|
||||
return None
|
||||
|
||||
async def transmission_data_updated(self, datas):
|
||||
torrent_stats = datas["data"]
|
||||
qs = (Torrent.objects
|
||||
.filter(Q(user_id=self.follow_user.id) | Q(shared_users=self.follow_user.id))
|
||||
.values_list("id", flat=True).distinct())
|
||||
torrent_ids = [i async for i in qs]
|
||||
|
||||
for hash_string, data in torrent_stats.items():
|
||||
if hash_string in torrent_ids:
|
||||
await self.send_json({
|
||||
"context": "transmission_data_updated",
|
||||
"data": data
|
||||
})
|
||||
|
||||
async def add_torrent(self, data):
|
||||
await self.send_json({
|
||||
"context": "add_torrent",
|
||||
"torrent_id": data["data"]
|
||||
})
|
||||
|
||||
async def remove_torrent(self, data):
|
||||
await self.send_json({
|
||||
"context": "remove_torrent",
|
||||
"torrent_id": data["data"]
|
||||
})
|
||||
|
||||
async def update_torrent(self, data):
|
||||
await self.send_json({
|
||||
"context": "update_torrent",
|
||||
"torrent_id": data["data"]["torrent_id"],
|
||||
"updated_fields": data["data"]["updated_fields"]
|
||||
})
|
||||
62
app/torrent/management/commands/torrent_event.py
Normal file
62
app/torrent/management/commands/torrent_event.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import close_old_connections
|
||||
|
||||
import time
|
||||
import signal
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from torrent.models import Torrent
|
||||
from torrent.utils import transmission_handler
|
||||
from app.utils import send_sync_channel_message
|
||||
|
||||
|
||||
def update_transmission_data():
|
||||
data = transmission_handler.get_all_data()
|
||||
|
||||
updated_torrents = []
|
||||
for torrent in Torrent.objects.all():
|
||||
if torrent.id in data and torrent.transmission_data != data[torrent.id]:
|
||||
torrent.transmission_data = data[torrent.id]
|
||||
updated_torrents.append(torrent)
|
||||
if updated_torrents:
|
||||
Torrent.objects.bulk_update(updated_torrents, ["transmission_data"])
|
||||
send_sync_channel_message("torrent", "transmission_data_updated", {
|
||||
torrent.id: torrent.transmission_data
|
||||
for torrent in updated_torrents
|
||||
})
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
task_schedule = {
|
||||
"update_transmission_data": {
|
||||
"func": update_transmission_data,
|
||||
"schedule": 5.0
|
||||
}
|
||||
}
|
||||
histories = {}
|
||||
run = True
|
||||
|
||||
def handle(self, *args, **options):
|
||||
signal.signal(signal.SIGINT, self.exit_gracefully)
|
||||
signal.signal(signal.SIGTERM, self.exit_gracefully)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("start"))
|
||||
while self.run:
|
||||
for name, task in self.task_schedule.items():
|
||||
if name not in self.histories or time.time() - self.histories[name] > task["schedule"]:
|
||||
self.call_func(name)
|
||||
time.sleep(0.5)
|
||||
|
||||
def exit_gracefully(self, signum, frame):
|
||||
self.stdout.write(self.style.SUCCESS("exit"))
|
||||
self.run = False
|
||||
|
||||
def call_func(self, name):
|
||||
close_old_connections()
|
||||
try:
|
||||
self.task_schedule[name]["func"]()
|
||||
self.histories[name] = time.time()
|
||||
except Exception as e:
|
||||
tb = traceback.format_exc()
|
||||
self.stderr.write(self.style.ERROR(f"Error in {name}: {e}\n{tb}"))
|
||||
40
app/torrent/migrations/0001_initial.py
Normal file
40
app/torrent/migrations/0001_initial.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-04 23:41
|
||||
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='File',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('rel_name', models.TextField()),
|
||||
('size', models.BigIntegerField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SharedUser',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Torrent',
|
||||
fields=[
|
||||
('id', models.CharField(max_length=40, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('date_added', models.DateTimeField(auto_now_add=True)),
|
||||
('size', models.PositiveBigIntegerField()),
|
||||
('transmission_data', models.JSONField(default=dict)),
|
||||
],
|
||||
),
|
||||
]
|
||||
47
app/torrent/migrations/0002_initial.py
Normal file
47
app/torrent/migrations/0002_initial.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-04 23:41
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('torrent', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='shareduser',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='torrent',
|
||||
name='shared_users',
|
||||
field=models.ManyToManyField(blank=True, related_name='torrents_shares', through='torrent.SharedUser', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='torrent',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='torrents', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shareduser',
|
||||
name='torrent',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='torrent.torrent'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='file',
|
||||
name='torrent',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='torrent.torrent'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='shareduser',
|
||||
unique_together={('user', 'torrent')},
|
||||
),
|
||||
]
|
||||
18
app/torrent/migrations/0003_torrent_date_modified.py
Normal file
18
app/torrent/migrations/0003_torrent_date_modified.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-10 16:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('torrent', '0002_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='torrent',
|
||||
name='date_modified',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-10 16:53
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('torrent', '0003_torrent_date_modified'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='shareduser',
|
||||
old_name='date',
|
||||
new_name='date_created',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-10 16:56
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('torrent', '0004_rename_date_shareduser_date_created'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='torrent',
|
||||
old_name='date_added',
|
||||
new_name='date_created',
|
||||
),
|
||||
]
|
||||
0
app/torrent/migrations/__init__.py
Normal file
0
app/torrent/migrations/__init__.py
Normal file
94
app/torrent/models.py
Normal file
94
app/torrent/models.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
import mimetypes
|
||||
import uuid
|
||||
import shlex
|
||||
|
||||
|
||||
class Torrent(models.Model):
|
||||
id = models.CharField(max_length=40, primary_key=True)
|
||||
name = models.CharField(max_length=255)
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
date_modified = models.DateTimeField(auto_now=True)
|
||||
user = models.ForeignKey("user.User", on_delete=models.CASCADE, related_name="torrents")
|
||||
shared_users = models.ManyToManyField("user.User", related_name="torrents_shares", blank=True, through="SharedUser")
|
||||
size = models.PositiveBigIntegerField()
|
||||
transmission_data = models.JSONField(default=dict)
|
||||
|
||||
@cached_property
|
||||
def len_files(self):
|
||||
if hasattr(self, "_len_files"):
|
||||
return self._len_files
|
||||
else:
|
||||
return File.objects.filter(torrent_id=self.id).count()
|
||||
|
||||
@property
|
||||
async def alen_files(self):
|
||||
if hasattr(self, "_len_files"):
|
||||
return self._len_files
|
||||
else:
|
||||
return await File.objects.filter(torrent_id=self.id).acount()
|
||||
|
||||
@cached_property
|
||||
def related_users(self):
|
||||
return [
|
||||
self.user_id,
|
||||
*self.shared_users.values_list("id", flat=True)
|
||||
]
|
||||
|
||||
|
||||
class SharedUser(models.Model):
|
||||
user = models.ForeignKey("user.User", models.CASCADE)
|
||||
torrent = models.ForeignKey("Torrent", models.CASCADE)
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ("user", "torrent")
|
||||
|
||||
|
||||
|
||||
class File(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
|
||||
torrent = models.ForeignKey("Torrent", models.CASCADE, related_name="files")
|
||||
rel_name = models.TextField()
|
||||
size = models.BigIntegerField()
|
||||
|
||||
@property
|
||||
def pathname(self):
|
||||
return Path(self.rel_name)
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
return self.pathname.name
|
||||
|
||||
@property
|
||||
def abs_pathname(self):
|
||||
return settings.DOWNLOAD_BASE_DIR / self.pathname
|
||||
|
||||
@property
|
||||
def mime_types(self):
|
||||
mime = mimetypes.guess_type(self.pathname)
|
||||
if mime:
|
||||
return mime
|
||||
else:
|
||||
return "application/octet-stream"
|
||||
|
||||
@property
|
||||
def is_stream_video(self):
|
||||
return self.pathname.stem in ["mp4", "flv", "webm"]
|
||||
|
||||
@property
|
||||
def is_video(self):
|
||||
return self.pathname.stem in ["mp4", "flv", "webm", "avi", "mkv"]
|
||||
|
||||
@property
|
||||
def accel_redirect(self):
|
||||
return shlex.quote(f"{settings.NGINX_ACCEL_BASE}/{self.pathname}")
|
||||
|
||||
@property
|
||||
def disposition(self):
|
||||
return f'attachment; filename="{quote(self.filename)}"; filename*="{quote(self.filename)}"'
|
||||
30
app/torrent/serializers.py
Normal file
30
app/torrent/serializers.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from user.serializers import UserSerializer
|
||||
from .models import Torrent, File
|
||||
|
||||
|
||||
class TorrentSerializer(serializers.ModelSerializer):
|
||||
count_files = serializers.IntegerField(read_only=True, source="len_files")
|
||||
download_url = serializers.SerializerMethodField(read_only=True)
|
||||
class Meta:
|
||||
model = Torrent
|
||||
fields = "__all__"
|
||||
|
||||
def get_download_url(self, obj):
|
||||
return reverse("torrent:download_torrent", kwargs={"torrent_id": obj.id})
|
||||
|
||||
|
||||
class FileSerializer(serializers.ModelSerializer):
|
||||
is_stream_video = serializers.BooleanField(read_only=True)
|
||||
is_video = serializers.BooleanField(read_only=True)
|
||||
download_url = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = File
|
||||
fields = "__all__"
|
||||
|
||||
def get_download_url(self, obj):
|
||||
return reverse("torrent:download_file", kwargs={"file_id": obj.id})
|
||||
47
app/torrent/signals.py
Normal file
47
app/torrent/signals.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from app.utils import send_sync_channel_message
|
||||
from .utils import transmission_handler
|
||||
from .models import Torrent, SharedUser
|
||||
|
||||
|
||||
def on_post_save_torrent(instance: Torrent, created, **kwargs):
|
||||
if created:
|
||||
send_sync_channel_message(f"user_{instance.user_id}", "add_torrent", instance.id)
|
||||
|
||||
|
||||
def on_pre_delete_torrent(instance: Torrent, **kwargs):
|
||||
transmission_handler.delete(instance.id)
|
||||
for user_id in instance.related_users:
|
||||
send_sync_channel_message(f"user_{user_id}", "remove_torrent", instance.id)
|
||||
|
||||
|
||||
def on_shared_user_changed(sender, instance: Torrent, action, pk_set, **kwargs):
|
||||
# print("on_share_user_changed", sender, instance, action, pk_set)
|
||||
# on_share_user_changed <class 'torrent.models.SharedUser'> Torrent object (a9164e99d5181cfef0c23c209334103619080908) pre_add {3}
|
||||
# on_share_user_changed <class 'torrent.models.SharedUser'> Torrent object (a9164e99d5181cfef0c23c209334103619080908) post_add {3}
|
||||
match action:
|
||||
case "pre_add":
|
||||
pass
|
||||
case "post_add":
|
||||
for user_id in pk_set:
|
||||
send_sync_channel_message(f"user_{user_id}", "add_torrent", instance.id)
|
||||
for user_id in instance.related_users:
|
||||
send_sync_channel_message(f"user_{user_id}", "update_torrent", {
|
||||
"torrent_id": instance.id,
|
||||
"updated_fields": {"shared_users": list(instance.shared_users.all().values_list("id", flat=True))}
|
||||
})
|
||||
case "pre_remove":
|
||||
pass
|
||||
case "post_remove":
|
||||
for user_id in pk_set:
|
||||
send_sync_channel_message(f"user_{user_id}", "remove_torrent", instance.id)
|
||||
for user_id in instance.related_users:
|
||||
send_sync_channel_message(f"user_{user_id}", "update_torrent", {
|
||||
"torrent_id": instance.id,
|
||||
"updated_fields": {"shared_users": list(instance.shared_users.all().values_list("id", flat=True))}
|
||||
})
|
||||
case "pre_clear":
|
||||
pass
|
||||
case "post_clear":
|
||||
pass
|
||||
case _:
|
||||
pass
|
||||
26
app/torrent/tasks.py
Normal file
26
app/torrent/tasks.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from django.db import close_old_connections
|
||||
from celery import shared_task
|
||||
from channels.layers import get_channel_layer
|
||||
from asgiref.sync import async_to_sync
|
||||
|
||||
from .models import Torrent, File
|
||||
from .utils import transmission_handler
|
||||
from app.utils import send_sync_channel_message
|
||||
|
||||
|
||||
@shared_task
|
||||
def update_transmission_data():
|
||||
data = transmission_handler.get_all_data()
|
||||
|
||||
updated_torrents = []
|
||||
for torrent in Torrent.objects.all():
|
||||
if torrent.id in data and torrent.transmission_data != data[torrent.id]:
|
||||
torrent.transmission_data = data[torrent.id]
|
||||
updated_torrents.append(torrent)
|
||||
if updated_torrents:
|
||||
Torrent.objects.bulk_update(updated_torrents, ["transmission_data"])
|
||||
send_sync_channel_message("torrent", "transmission_data_updated", {
|
||||
torrent.id: torrent.transmission_data
|
||||
for torrent in updated_torrents
|
||||
})
|
||||
|
||||
9
app/torrent/templates/torrent/home.html
Normal file
9
app/torrent/templates/torrent/home.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends "torrent/layout.html" %}
|
||||
{% load django_vite %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
const current_user = {{ request.user.min_infos|safe }};
|
||||
</script>
|
||||
{% vite_asset "app/torrent.js" %}
|
||||
{% endblock %}
|
||||
1
app/torrent/templates/torrent/layout.html
Normal file
1
app/torrent/templates/torrent/layout.html
Normal file
@@ -0,0 +1 @@
|
||||
{% extends "base.html" %}
|
||||
3
app/torrent/tests.py
Normal file
3
app/torrent/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
10
app/torrent/urls.py
Normal file
10
app/torrent/urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import HomeView, download_file, download_torrent
|
||||
|
||||
app_name = "torrent"
|
||||
urlpatterns = [
|
||||
path("", HomeView.as_view(), name="home"),
|
||||
path("download_file/<uuid:file_id>", download_file, name="download_file"),
|
||||
path("download_torrent/<str:torrent_id>", download_torrent, name="download_torrent"),
|
||||
]
|
||||
119
app/torrent/utils.py
Normal file
119
app/torrent/utils.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from django.conf import settings
|
||||
|
||||
import traceback
|
||||
from transmission_rpc import Client
|
||||
from transmission_rpc.error import TransmissionError
|
||||
|
||||
# from app.utils import send_sync_channel_message
|
||||
from .models import Torrent, File
|
||||
from user.models import User
|
||||
|
||||
|
||||
class Transmission:
|
||||
trpc_args = [
|
||||
"id", "percentDone", "uploadRatio", "rateUpload", "rateDownload", "hashString", "status", "sizeWhenDone",
|
||||
"leftUntilDone", "name", "eta", "totalSize"
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.client = Client(**settings.TRANSMISSION)
|
||||
|
||||
def add_torrent(self, file):
|
||||
return self.client.add_torrent(file)
|
||||
|
||||
def get_data(self, hash_string):
|
||||
data = self.client.get_torrent(hash_string, self.trpc_args)
|
||||
|
||||
return {
|
||||
"progress": data.progress,
|
||||
**data.fields
|
||||
}
|
||||
|
||||
def get_all_data(self, hash_strings=None):
|
||||
return {
|
||||
data.hashString: {"progress": data.progress, **data.fields}
|
||||
for data in self.client.get_torrents(hash_strings, self.trpc_args)
|
||||
}
|
||||
|
||||
def get_files(self, hash_string):
|
||||
return self.client.get_torrent(hash_string).get_files()
|
||||
|
||||
def delete(self, hash_string):
|
||||
return self.client.remove_torrent(hash_string, delete_data=True)
|
||||
|
||||
|
||||
transmission_handler = Transmission()
|
||||
|
||||
|
||||
def torrent_proceed(user, file):
|
||||
r = {
|
||||
"torrent": None,
|
||||
"status": "error",
|
||||
"message": "Unexpected error"
|
||||
}
|
||||
|
||||
user: User
|
||||
if user.size_used > user.max_size:
|
||||
r["message"] = "Size exceed"
|
||||
return r
|
||||
|
||||
try:
|
||||
torrent_uploaded = transmission_handler.add_torrent(file)
|
||||
except TransmissionError:
|
||||
print(traceback.format_exc())
|
||||
r["message"] = "Transmission Error"
|
||||
return r
|
||||
except:
|
||||
print(traceback.format_exc())
|
||||
return r
|
||||
else:
|
||||
r["status"] = "warn"
|
||||
qs = Torrent.objects.filter(pk=torrent_uploaded.hashString)
|
||||
if qs.exists():
|
||||
torrent = qs.get()
|
||||
if torrent.user == user:
|
||||
r["message"] = "Already exist"
|
||||
return r
|
||||
elif torrent.shared_users.filter(user=user).exists():
|
||||
r["message"] = "Already shared"
|
||||
return r
|
||||
else:
|
||||
torrent.shared_users.add(user)
|
||||
r["status"] = "success"
|
||||
r["message"] = "Torrent downloaded by an other user, added to your list"
|
||||
return r
|
||||
else:
|
||||
data = transmission_handler.get_data(torrent_uploaded.hashString)
|
||||
torrent = Torrent.objects.create(
|
||||
id=data["hashString"],
|
||||
name=data["name"],
|
||||
user=user,
|
||||
size=data["totalSize"],
|
||||
transmission_data=data
|
||||
)
|
||||
File.objects.bulk_create([
|
||||
File(
|
||||
torrent=torrent,
|
||||
rel_name=file.name,
|
||||
size=file.size,
|
||||
)
|
||||
for file in transmission_handler.get_files(torrent.id)
|
||||
])
|
||||
|
||||
r["torrent"] = torrent
|
||||
r["status"] = "success"
|
||||
r["message"] = "Torrent added"
|
||||
return r
|
||||
|
||||
|
||||
def torrent_share(torrent, current_user, target_user_id):
|
||||
from .models import Torrent, SharedUser
|
||||
|
||||
torrent: Torrent
|
||||
|
||||
if (torrent.user_id != target_user_id and
|
||||
any([torrent.user == current_user, torrent.shared_users.filter(id=current_user.id)]) and
|
||||
not SharedUser.objects.filter(torrent_id=torrent.id, user_id=target_user_id).exists()):
|
||||
torrent.shared_users.add(target_user_id)
|
||||
return True
|
||||
return False
|
||||
157
app/torrent/views.py
Normal file
157
app/torrent/views.py
Normal file
@@ -0,0 +1,157 @@
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.views.generic import TemplateView
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Q, Count, OuterRef
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.http import HttpResponse, Http404, StreamingHttpResponse
|
||||
|
||||
import aiofiles
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from rest_framework import mixins
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from app.utils import StreamingZipFileResponse
|
||||
from user.models import User
|
||||
from .models import Torrent, File, SharedUser
|
||||
from .serializers import TorrentSerializer, FileSerializer
|
||||
from .utils import torrent_proceed, torrent_share
|
||||
|
||||
|
||||
class HomeView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "torrent/home.html"
|
||||
|
||||
|
||||
async def download_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
|
||||
)
|
||||
|
||||
try:
|
||||
file = await qs.aget()
|
||||
except File.DoesNotExist:
|
||||
raise Http404()
|
||||
else:
|
||||
if request.GET.get("dl_hotfix", "0") == "1":
|
||||
async def read_file():
|
||||
async with aiofiles.open(file.abs_pathname, "rb") as f:
|
||||
while chunk := await f.read(128 * 1024):
|
||||
yield chunk
|
||||
response = StreamingHttpResponse(read_file())
|
||||
response["Content-Length"] = file.size
|
||||
response["Content-Type"] = "application/octet-stream"
|
||||
response["Content-Disposition"] = file.disposition
|
||||
return response
|
||||
else:
|
||||
response = HttpResponse()
|
||||
response["X-Accel-Redirect"] = file.accel_redirect
|
||||
response["Content-Type"] = file.mime_types
|
||||
response["Content-Disposition"] = file.disposition
|
||||
return response
|
||||
|
||||
|
||||
async def download_torrent(request, torrent_id):
|
||||
# py version
|
||||
user = await request.auser()
|
||||
qs = Torrent.objects.filter(
|
||||
Q(user=user)
|
||||
| Q(shared_users=user)
|
||||
| Q(user__friends=user)
|
||||
| Q(shared_users__friends=user),
|
||||
transmission_data__progress__gte=100,
|
||||
pk=torrent_id
|
||||
).annotate(count_files=Count("files"))
|
||||
|
||||
torrent = await qs.aget()
|
||||
|
||||
if torrent.count_files == 1:
|
||||
file = await torrent.files.afirst()
|
||||
return redirect(reverse("torrent:download_file", kwargs={
|
||||
"file_id": file.pk
|
||||
}))
|
||||
|
||||
response = StreamingZipFileResponse(
|
||||
filename="test.zip",
|
||||
file_list=[
|
||||
(file.abs_pathname, file.rel_name)
|
||||
async for file in torrent.files.all()
|
||||
],
|
||||
is_async=True
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
class TorrentViewSet(mixins.CreateModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet):
|
||||
queryset = Torrent.objects.all().annotate(count_files=Count("files"))
|
||||
serializer_class = TorrentSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
|
||||
qs.filter(
|
||||
Q(user=self.request.user)
|
||||
| Q(shared_users=self.request.user)
|
||||
| Q(user__friends=self.request.user)
|
||||
| Q(shared_users__friends=self.request.user)
|
||||
)
|
||||
|
||||
# Récupération des torrents de l'utilisateur et de ceux partagé à celui-ci (ordonné par ordre de partage ou par date d'ajout du torrent)
|
||||
user_id = self.request.query_params.get("user", None)
|
||||
if user_id:
|
||||
qs = qs.filter(Q(user_id=user_id) | Q(shared_users=user_id))
|
||||
else:
|
||||
user_id = self.request.user.id
|
||||
|
||||
sub = SharedUser.objects.filter(torrent_id=OuterRef("pk"), user_id=user_id).values("date_created")
|
||||
qs = qs.annotate(last_date=Coalesce(sub, "date_created")).order_by("-last_date")
|
||||
|
||||
return qs
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
file = request.data["file"]
|
||||
r = torrent_proceed(self.request.user, file)
|
||||
if r["torrent"]:
|
||||
r["torrent"] = self.get_serializer_class()(r["torrent"]).data
|
||||
return Response(r)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
instance: Torrent
|
||||
if instance.user == self.request.user:
|
||||
return super().perform_destroy(instance)
|
||||
else:
|
||||
if instance.shared_users.filter(id=self.request.user.id).exists():
|
||||
instance.shared_users.remove(self.request.user)
|
||||
|
||||
@action(methods=["post"], detail=True)
|
||||
def share(self, request, pk):
|
||||
user_id = self.request.data.get("user_id")
|
||||
torrent = self.get_object()
|
||||
is_share_success = torrent_share(torrent=torrent, current_user=self.request.user, target_user_id=user_id)
|
||||
return Response({"success": is_share_success})
|
||||
|
||||
@action(methods=["get"], detail=False)
|
||||
def user_stats(self, request):
|
||||
Torrent.objects.filter(user=self.request.user).aggregate(total_size=Sum("size"))
|
||||
|
||||
|
||||
class FileViewSet(mixins.RetrieveModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet):
|
||||
queryset = File.objects.all()
|
||||
serializer_class = FileSerializer
|
||||
filterset_fields = ["torrent"]
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
return qs
|
||||
Reference in New Issue
Block a user