diff --git a/app/api/tests.py b/app/api/tests.py index 7ce503c..968a851 100644 --- a/app/api/tests.py +++ b/app/api/tests.py @@ -1,3 +1,67 @@ from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APITestCase, APIClient +from rest_framework import status -# Create your tests here. +from .routers import router + + +class RouterTestCase(TestCase): + def test_router_urls(self): + """Test that the router generates the expected URL patterns""" + url_patterns = router.urls + + # Check that all expected viewsets are registered + expected_basenames = ['user', 'torrent', 'file', 'friend-request'] + registered_basenames = [url.name.split('-')[0] for url in url_patterns if '-list' in url.name] + + for basename in expected_basenames: + self.assertIn(basename, registered_basenames) + + # Check that list and detail URLs are generated for each viewset + for basename in expected_basenames: + list_url_name = f"{basename}-list" + detail_url_name = f"{basename}-detail" + + self.assertTrue(any(url.name == list_url_name for url in url_patterns)) + self.assertTrue(any(url.name == detail_url_name for url in url_patterns)) + + +class APIEndpointsTestCase(APITestCase): + def setUp(self): + from user.models import User + + # Create a test user + self.user = User.objects.create_user( + username='testuser', + email='test@example.com', + password='testpassword' + ) + + # Authenticate the client + self.client = APIClient() + self.client.force_authenticate(user=self.user) + + def test_user_endpoint(self): + """Test that the users endpoint is accessible""" + url = reverse('user-list') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_torrent_endpoint(self): + """Test that the torrents endpoint is accessible""" + url = reverse('torrent-list') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_file_endpoint(self): + """Test that the files endpoint is accessible""" + url = reverse('file-list') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_friend_request_endpoint(self): + """Test that the friend requests endpoint is accessible""" + url = reverse('friendrequest-list') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/app/frontend/src/components/torrent/App.vue b/app/frontend/src/components/torrent/App.vue index d69f367..fac9fac 100644 --- a/app/frontend/src/components/torrent/App.vue +++ b/app/frontend/src/components/torrent/App.vue @@ -15,7 +15,7 @@ - {{dm_status}} + @@ -157,6 +157,7 @@ export default { this.dm_status = data; }) this.$qt.on("upload_torrent", data => { + console.log(typeof data); this.uploadTorrentB64(data); }) this.$qt.on("files_updated", data => { @@ -220,7 +221,8 @@ export default { }, async uploadTorrentB64(b64_torrent){ let form = new FormData(); - form.append("torrent", b64_torrent); + form.append("file", b64_torrent); + form.append("file_mode", "base64"); let response = await fetch("/api/torrents/", { method: "POST", headers: { diff --git a/app/frontend/src/components/torrent/DM.vue b/app/frontend/src/components/torrent/DM.vue index c10b493..e8d06d2 100644 --- a/app/frontend/src/components/torrent/DM.vue +++ b/app/frontend/src/components/torrent/DM.vue @@ -6,12 +6,19 @@ + @@ -97,9 +97,10 @@ export default { 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){ + }else if(this.dm_status.downloading.includes(file_id)){ downloading[file_id] = file; if(file_id in this.dm_status["downloader_stats"]){ + downloading[file_id]["stats"] = this.dm_status["downloader_stats"][file_id]; }else{ downloading[file_id]["stats"] = { diff --git a/app/torrent/tests.py b/app/torrent/tests.py index 7ce503c..516abd4 100644 --- a/app/torrent/tests.py +++ b/app/torrent/tests.py @@ -1,3 +1,328 @@ from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APITestCase, APIClient +from rest_framework import status +from django.conf import settings +from unittest.mock import patch, MagicMock -# Create your tests here. +from .models import Torrent, SharedUser, File +from user.models import User +from .views import TorrentViewSet, FileViewSet +from .utils import Transmission, torrent_proceed, torrent_share + + +class TorrentModelTestCase(TestCase): + def setUp(self): + self.user = User.objects.create_user( + username='testuser', + email='test@example.com', + password='testpassword' + ) + self.torrent = Torrent.objects.create( + id='abc123', + name='Test Torrent', + user=self.user, + size=1000, + transmission_data={} + ) + self.shared_user = User.objects.create_user( + username='shareduser', + email='shared@example.com', + password='sharedpassword' + ) + self.file = File.objects.create( + torrent=self.torrent, + rel_name='test_file.txt', + size=100 + ) + + def test_len_files(self): + """Test the len_files property returns the correct count of files""" + self.assertEqual(self.torrent.len_files, 1) + + # Add another file and test again + File.objects.create( + torrent=self.torrent, + rel_name='another_file.txt', + size=200 + ) + + # Clear cached_property + if hasattr(self.torrent, '_len_files'): + delattr(self.torrent, '_len_files') + + self.assertEqual(self.torrent.len_files, 2) + + def test_related_users(self): + """Test the related_users property returns the correct list of users""" + # Initially only the owner + self.assertEqual(self.torrent.related_users, [self.user.id]) + + # Add a shared user + self.torrent.shared_users.add(self.shared_user) + + # Clear cached_property + if hasattr(self.torrent, '_related_users'): + delattr(self.torrent, '_related_users') + + # Should include both users now + self.assertIn(self.user.id, self.torrent.related_users) + self.assertIn(self.shared_user.id, self.torrent.related_users) + + +class FileModelTestCase(TestCase): + def setUp(self): + self.user = User.objects.create_user( + username='testuser', + email='test@example.com', + password='testpassword' + ) + self.torrent = Torrent.objects.create( + id='abc123', + name='Test Torrent', + user=self.user, + size=1000, + transmission_data={} + ) + self.file = File.objects.create( + torrent=self.torrent, + rel_name='test/path/file.mp4', + size=100 + ) + + def test_pathname(self): + """Test the pathname property returns the correct path""" + self.assertEqual(str(self.file.pathname), 'test/path/file.mp4') + + def test_filename(self): + """Test the filename property returns the correct filename""" + self.assertEqual(self.file.filename, 'file.mp4') + + def test_abs_pathname(self): + """Test the abs_pathname property returns the correct absolute path""" + expected_path = settings.DOWNLOAD_BASE_DIR / self.file.pathname + self.assertEqual(self.file.abs_pathname, expected_path) + + def test_is_video(self): + """Test the is_video property correctly identifies video files""" + self.assertTrue(self.file.is_video) # mp4 should be identified as video + + # Test non-video file + non_video_file = File.objects.create( + torrent=self.torrent, + rel_name='test/path/document.pdf', + size=50 + ) + self.assertFalse(non_video_file.is_video) + + +class SharedUserModelTestCase(TestCase): + def setUp(self): + self.owner = User.objects.create_user( + username='owner', + email='owner@example.com', + password='ownerpassword' + ) + self.shared_user = User.objects.create_user( + username='shareduser', + email='shared@example.com', + password='sharedpassword' + ) + self.torrent = Torrent.objects.create( + id='abc123', + name='Test Torrent', + user=self.owner, + size=1000, + transmission_data={} + ) + + def test_shared_user_creation(self): + """Test creating a shared user relationship""" + shared = SharedUser.objects.create( + user=self.shared_user, + torrent=self.torrent + ) + self.assertEqual(shared.user, self.shared_user) + self.assertEqual(shared.torrent, self.torrent) + + # Verify the relationship is reflected in the torrent's shared_users + self.assertIn(self.shared_user, self.torrent.shared_users.all()) + + +class TorrentViewSetTestCase(APITestCase): + def setUp(self): + self.user = User.objects.create_user( + username='testuser', + email='test@example.com', + password='testpassword' + ) + self.client = APIClient() + self.client.force_authenticate(user=self.user) + + self.torrent = Torrent.objects.create( + id='abc123', + name='Test Torrent', + user=self.user, + size=1000, + transmission_data={} + ) + + self.file = File.objects.create( + torrent=self.torrent, + rel_name='test_file.txt', + size=100 + ) + + def test_list_torrents(self): + """Test listing torrents""" + url = reverse('torrent-list') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['id'], self.torrent.id) + + def test_retrieve_torrent(self): + """Test retrieving a specific torrent""" + url = reverse('torrent-detail', args=[self.torrent.id]) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['id'], self.torrent.id) + self.assertEqual(response.data['name'], 'Test Torrent') + + @patch('torrent.views.torrent_share') + def test_share_torrent(self, mock_torrent_share): + """Test sharing a torrent with another user""" + mock_torrent_share.return_value = True + + shared_user = User.objects.create_user( + username='shareduser', + email='shared@example.com', + password='sharedpassword' + ) + + url = reverse('torrent-share', args=[self.torrent.id]) + response = self.client.post(url, {'user_id': shared_user.id}) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(response.data['success']) + mock_torrent_share.assert_called_once() + + +class FileViewSetTestCase(APITestCase): + def setUp(self): + self.user = User.objects.create_user( + username='testuser', + email='test@example.com', + password='testpassword' + ) + self.client = APIClient() + self.client.force_authenticate(user=self.user) + + self.torrent = Torrent.objects.create( + id='abc123', + name='Test Torrent', + user=self.user, + size=1000, + transmission_data={} + ) + + self.file = File.objects.create( + torrent=self.torrent, + rel_name='test_file.txt', + size=100 + ) + + def test_list_files(self): + """Test listing files""" + url = reverse('file-list') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_retrieve_file(self): + """Test retrieving a specific file""" + url = reverse('file-detail', args=[self.file.id]) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['id'], str(self.file.id)) + self.assertEqual(response.data['rel_name'], 'test_file.txt') + + +class TransmissionUtilsTestCase(TestCase): + @patch('torrent.utils.Client') + def test_transmission_init(self, mock_client): + """Test Transmission class initialization""" + transmission = Transmission() + mock_client.assert_called_once_with(**settings.TRANSMISSION) + + @patch('torrent.utils.Client') + def test_add_torrent(self, mock_client): + """Test adding a torrent""" + mock_instance = mock_client.return_value + mock_instance.add_torrent.return_value = MagicMock() + + transmission = Transmission() + file_obj = MagicMock() + result = transmission.add_torrent(file_obj) + + mock_instance.add_torrent.assert_called_once_with(file_obj) + self.assertEqual(result, mock_instance.add_torrent.return_value) + + @patch('torrent.utils.Client') + def test_get_data(self, mock_client): + """Test getting torrent data""" + mock_instance = mock_client.return_value + mock_torrent = MagicMock() + mock_torrent.progress = 50 + mock_torrent.fields = {'name': 'Test', 'size': 1000} + mock_instance.get_torrent.return_value = mock_torrent + + transmission = Transmission() + result = transmission.get_data('hash123') + + mock_instance.get_torrent.assert_called_once_with('hash123', transmission.trpc_args) + self.assertEqual(result['progress'], 50) + self.assertEqual(result['name'], 'Test') + self.assertEqual(result['size'], 1000) + + +class TorrentProceedTestCase(TestCase): + def setUp(self): + self.user = User.objects.create_user( + username='testuser', + email='test@example.com', + password='testpassword', + max_size=10000 + ) + + @patch('torrent.utils.transmission_handler') + def test_torrent_proceed_size_exceed(self, mock_transmission): + """Test torrent_proceed when user size is exceeded""" + # Set user's used size to exceed max_size + self.user.max_size = 100 + Torrent.objects.create( + id='abc123', + name='Test Torrent', + user=self.user, + size=200, # Exceeds max_size + transmission_data={} + ) + + file_obj = MagicMock() + result = torrent_proceed(self.user, file_obj) + + self.assertEqual(result['status'], 'error') + self.assertEqual(result['message'], 'Size exceed') + mock_transmission.add_torrent.assert_not_called() + + @patch('torrent.utils.transmission_handler') + def test_torrent_proceed_transmission_error(self, mock_transmission): + """Test torrent_proceed when transmission raises an error""" + from transmission_rpc.error import TransmissionError + + mock_transmission.add_torrent.side_effect = TransmissionError('Test error') + + file_obj = MagicMock() + result = torrent_proceed(self.user, file_obj) + + self.assertEqual(result['status'], 'error') + self.assertEqual(result['message'], 'Transmission Error') diff --git a/app/torrent/utils.py b/app/torrent/utils.py index 0fc3af4..8d3c820 100644 --- a/app/torrent/utils.py +++ b/app/torrent/utils.py @@ -1,6 +1,9 @@ from django.conf import settings import traceback +import base64 +import io + from transmission_rpc import Client from transmission_rpc.error import TransmissionError @@ -18,8 +21,15 @@ class Transmission: def __init__(self): self.client = Client(**settings.TRANSMISSION) - def add_torrent(self, file): - return self.client.add_torrent(file) + def add_torrent(self, file, file_mode="file_object"): + match file_mode: + case "file_object": + return self.client.add_torrent(file) + case "base64": + file_content = base64.b64decode(file) + file_obj = io.BytesIO(file_content) + return self.client.add_torrent(file_obj) + return None def get_data(self, hash_string): data = self.client.get_torrent(hash_string, self.trpc_args) @@ -45,7 +55,7 @@ class Transmission: transmission_handler = Transmission() -def torrent_proceed(user, file): +def torrent_proceed(user, file, file_mode="file_object"): r = { "torrent": None, "status": "error", @@ -58,7 +68,7 @@ def torrent_proceed(user, file): return r try: - torrent_uploaded = transmission_handler.add_torrent(file) + torrent_uploaded = transmission_handler.add_torrent(file, file_mode=file_mode) except TransmissionError: print(traceback.format_exc()) r["message"] = "Transmission Error" @@ -74,7 +84,7 @@ def torrent_proceed(user, file): if torrent.user == user: r["message"] = "Already exist" return r - elif torrent.shared_users.filter(user=user).exists(): + elif torrent.shared_users.filter(id=user.id).exists(): r["message"] = "Already shared" return r else: diff --git a/app/torrent/views.py b/app/torrent/views.py index 969b0d8..033ef1e 100644 --- a/app/torrent/views.py +++ b/app/torrent/views.py @@ -2,7 +2,7 @@ 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 import Q, Count, OuterRef, Sum from django.db.models.functions import Coalesce from django.http import HttpResponse, Http404, StreamingHttpResponse @@ -152,7 +152,9 @@ class TorrentViewSet(mixins.CreateModelMixin, def create(self, request, *args, **kwargs): file = request.data["file"] - r = torrent_proceed(self.request.user, file) + file_mode = request.data.get("file_mode", "file_object") + r = torrent_proceed(self.request.user, file, file_mode) + if r["torrent"]: r["torrent"] = self.get_serializer_class()(r["torrent"]).data return Response(r) diff --git a/app/user/tests.py b/app/user/tests.py index 7ce503c..3e82271 100644 --- a/app/user/tests.py +++ b/app/user/tests.py @@ -1,3 +1,316 @@ from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APITestCase, APIClient +from rest_framework import status +from unittest.mock import patch, MagicMock -# Create your tests here. +from .models import User, FriendRequest, Invitation, UsernameUserManager +from torrent.models import Torrent +from .views import UserViewSet, FriendRequestViewSet + + +class UserModelTestCase(TestCase): + def setUp(self): + self.user = User.objects.create_user( + username='testuser', + email='test@example.com', + password='testpassword', + max_size=1000000 + ) + self.friend = User.objects.create_user( + username='frienduser', + email='friend@example.com', + password='friendpassword' + ) + + def test_size_used_property(self): + """Test the size_used property returns the correct total size of user's torrents""" + # Initially no torrents, so size should be 0 + self.assertEqual(self.user.size_used, 0) + + # Create a torrent for the user + Torrent.objects.create( + id='abc123', + name='Test Torrent', + user=self.user, + size=5000, + transmission_data={} + ) + + # Create another torrent + Torrent.objects.create( + id='def456', + name='Another Torrent', + user=self.user, + size=3000, + transmission_data={} + ) + + # Clear cached_property if it exists + if hasattr(self.user, 'total_size'): + delattr(self.user, 'total_size') + + # Size used should be the sum of torrent sizes + self.assertEqual(self.user.size_used, 8000) + + def test_min_infos_property(self): + """Test the min_infos property returns the correct user info""" + expected_info = { + 'username': 'testuser', + 'id': self.user.id + } + self.assertEqual(self.user.min_infos, expected_info) + + +class UsernameUserManagerTestCase(TestCase): + def test_create_user(self): + """Test creating a regular user""" + user = User.objects.create_user( + username='newuser', + email='new@example.com', + password='newpassword' + ) + self.assertFalse(user.is_staff) + self.assertFalse(user.is_superuser) + self.assertEqual(user.username, 'newuser') + self.assertEqual(user.email, 'new@example.com') + self.assertTrue(user.check_password('newpassword')) + + def test_create_superuser(self): + """Test creating a superuser""" + admin = User.objects.create_superuser( + username='admin', + email='admin@example.com', + password='adminpassword' + ) + self.assertTrue(admin.is_staff) + self.assertTrue(admin.is_superuser) + self.assertEqual(admin.username, 'admin') + self.assertEqual(admin.email, 'admin@example.com') + + def test_create_user_without_username(self): + """Test creating a user without username raises error""" + with self.assertRaises(ValueError): + User.objects.create_user( + username='', + email='test@example.com', + password='testpassword' + ) + + def test_create_user_without_email(self): + """Test creating a user without email raises error""" + with self.assertRaises(ValueError): + User.objects.create_user( + username='testuser', + email='', + password='testpassword' + ) + + +class FriendRequestModelTestCase(TestCase): + def setUp(self): + self.sender = User.objects.create_user( + username='sender', + email='sender@example.com', + password='senderpassword' + ) + self.receiver = User.objects.create_user( + username='receiver', + email='receiver@example.com', + password='receiverpassword' + ) + + def test_friend_request_creation(self): + """Test creating a friend request""" + friend_request = FriendRequest.objects.create( + sender=self.sender, + receiver=self.receiver + ) + self.assertEqual(friend_request.sender, self.sender) + self.assertEqual(friend_request.receiver, self.receiver) + + def test_unique_together_constraint(self): + """Test that the unique_together constraint works""" + FriendRequest.objects.create( + sender=self.sender, + receiver=self.receiver + ) + + # Creating another request with the same sender and receiver should raise an error + with self.assertRaises(Exception): + FriendRequest.objects.create( + sender=self.sender, + receiver=self.receiver + ) + + +class InvitationModelTestCase(TestCase): + def setUp(self): + self.creator = User.objects.create_user( + username='creator', + email='creator@example.com', + password='creatorpassword' + ) + + def test_invitation_creation(self): + """Test creating an invitation""" + invitation = Invitation.objects.create( + created_by=self.creator + ) + self.assertEqual(invitation.created_by, self.creator) + self.assertIsNotNone(invitation.token) + self.assertIsNone(invitation.user) + + def test_invitation_assignment(self): + """Test assigning an invitation to a user""" + invitation = Invitation.objects.create( + created_by=self.creator + ) + + new_user = User.objects.create_user( + username='newuser', + email='new@example.com', + password='newpassword' + ) + + invitation.user = new_user + invitation.save() + + # Refresh from database + invitation.refresh_from_db() + self.assertEqual(invitation.user, new_user) + + +class UserViewSetTestCase(APITestCase): + def setUp(self): + self.user = User.objects.create_user( + username='testuser', + email='test@example.com', + password='testpassword' + ) + self.friend = User.objects.create_user( + username='frienduser', + email='friend@example.com', + password='friendpassword' + ) + self.client = APIClient() + self.client.force_authenticate(user=self.user) + + def test_list_users(self): + """Test listing users""" + url = reverse('user-list') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 2) # Should include both users + + def test_retrieve_user(self): + """Test retrieving a specific user""" + url = reverse('user-detail', args=[self.friend.id]) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['username'], 'frienduser') + + def test_add_friend_request(self): + """Test adding a friend request""" + url = reverse('user-add-friend-request', args=[self.friend.username]) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(response.data['success']) + self.assertEqual(response.data['message'], 'Request sent') + + # Verify the friend request was created + self.assertTrue(FriendRequest.objects.filter( + sender=self.user, + receiver=self.friend + ).exists()) + + def test_add_friend_request_nonexistent_user(self): + """Test adding a friend request to a nonexistent user""" + url = reverse('user-add-friend-request', args=['nonexistentuser']) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertFalse(response.data['success']) + self.assertEqual(response.data['message'], "User 'nonexistentuser' doesn't exist") + + def test_remove_friend(self): + """Test removing a friend""" + # First add as friend + self.user.friends.add(self.friend) + + url = reverse('user-remove-friend', args=[self.friend.id]) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(response.data['success']) + + # Verify the friend was removed + self.assertFalse(self.user.friends.filter(id=self.friend.id).exists()) + + @patch('user.views.shutil.disk_usage') + def test_user_stats(self, mock_disk_usage): + """Test getting user stats""" + # Mock disk_usage return value + mock_disk_usage.return_value = MagicMock( + total=1000000, + used=500000, + free=500000 + ) + + # Create torrents for the user + Torrent.objects.create( + id='abc123', + name='Test Torrent', + user=self.user, + size=5000, + transmission_data={} + ) + + url = reverse('user-user-stats') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Check that the response contains the expected fields + self.assertIn('torrents_size', response.data) + self.assertIn('torrents_len', response.data) + self.assertIn('user_max_size', response.data) + self.assertIn('disk_total', response.data) + self.assertIn('disk_used', response.data) + self.assertIn('disk_free', response.data) + + +class FriendRequestViewSetTestCase(APITestCase): + def setUp(self): + self.user = User.objects.create_user( + username='testuser', + email='test@example.com', + password='testpassword' + ) + self.sender = User.objects.create_user( + username='sender', + email='sender@example.com', + password='senderpassword' + ) + self.client = APIClient() + self.client.force_authenticate(user=self.user) + + # Create a friend request + self.friend_request = FriendRequest.objects.create( + sender=self.sender, + receiver=self.user + ) + + def test_list_friend_requests(self): + """Test listing friend requests""" + url = reverse('friendrequest-list') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['sender']['username'], 'sender') + + def test_delete_friend_request(self): + """Test deleting a friend request""" + url = reverse('friendrequest-detail', args=[self.friend_request.id]) + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + # Verify the friend request was deleted + self.assertFalse(FriendRequest.objects.filter(id=self.friend_request.id).exists()) diff --git a/app/user/views.py b/app/user/views.py index 29dcb58..59938b8 100644 --- a/app/user/views.py +++ b/app/user/views.py @@ -98,7 +98,7 @@ class UserViewSet(mixins.RetrieveModelMixin, stats = User.objects.filter(id=request.user.id).aggregate( total_size=Sum("torrents__size"), total_torrent=Count("torrents"), - total_shared_torrent=Count("torrents_shares") + total_shared_torrent=Count("torrents_shares", distinct=True), ) disk_usage = shutil.disk_usage("/") diff --git a/docker-compose.yml b/docker-compose.yml index 838594c..63f153f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: redis: - image: redis:alpine + image: valkey/valkey:alpine restart: unless-stopped web: