增加通过用户uniqueId获取播放量前十的视频功能

This commit is contained in:
wanjia 2025-03-05 17:59:31 +08:00
parent 2713cba136
commit 4ccfc764f7
31 changed files with 1797 additions and 1313 deletions

View File

@ -94,6 +94,9 @@ DATABASES = {
'PASSWORD': '123456',
'HOST': 'localhost',
'PORT': '3306',
'OPTIONS': {
'charset': 'utf8mb4',
},
}
}

View File

@ -24,4 +24,5 @@ urlpatterns = [
path('admin/', admin.site.urls),
path('monitor/', include('monitor.urls')),
path('', lambda request: redirect('monitor/')), # 将根路径重定向到 monitor/
path('', include('monitor.urls')), # 假设您的应用名为 'monitor'
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,33 @@
# Generated by Django 5.1.6 on 2025-03-05 05:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('monitor', '0003_allresourceprocess'),
]
operations = [
migrations.CreateModel(
name='DouyinUserVideos',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sec_user_id', models.CharField(max_length=100, unique=True, verbose_name='用户ID')),
('nickname', models.CharField(max_length=100, verbose_name='昵称')),
('signature', models.TextField(blank=True, verbose_name='签名')),
('follower_count', models.IntegerField(default=0, verbose_name='粉丝数')),
('total_favorited', models.IntegerField(default=0, verbose_name='获赞数')),
('avatar_url', models.CharField(blank=True, max_length=255, verbose_name='头像URL')),
('video_paths', models.TextField(blank=True, verbose_name='视频路径JSON')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '抖音用户视频',
'verbose_name_plural': '抖音用户视频',
'db_table': 'douyin_user_videos',
},
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.1.6 on 2025-03-05 05:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('monitor', '0004_douyinuservideos'),
]
operations = [
migrations.AddField(
model_name='douyinuservideos',
name='videos_folder',
field=models.CharField(blank=True, max_length=255, verbose_name='视频文件夹路径'),
),
migrations.AlterField(
model_name='douyinuservideos',
name='video_paths',
field=models.TextField(blank=True, verbose_name='视频信息JSON'),
),
]

View File

@ -0,0 +1,36 @@
# Generated by Django 5.1.6 on 2025-03-05 06:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('monitor', '0005_douyinuservideos_videos_folder_and_more'),
]
operations = [
migrations.CreateModel(
name='TiktokUserVideos',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sec_user_id', models.CharField(max_length=255, unique=True)),
('nickname', models.CharField(max_length=255)),
('signature', models.TextField(blank=True, null=True)),
('follower_count', models.IntegerField(default=0)),
('total_favorited', models.IntegerField(default=0)),
('avatar_url', models.URLField(blank=True, null=True)),
('videos_folder', models.CharField(max_length=255)),
('video_paths', models.TextField(blank=True, null=True)),
('create_time', models.DateTimeField(auto_now_add=True)),
('update_time', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'TikTok用户视频',
'verbose_name_plural': 'TikTok用户视频',
},
),
migrations.DeleteModel(
name='DouyinUserVideos',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-03-05 06:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('monitor', '0006_tiktokuservideos_delete_douyinuservideos'),
]
operations = [
migrations.AlterField(
model_name='tiktokuservideos',
name='avatar_url',
field=models.TextField(blank=True, null=True),
),
]

View File

@ -73,3 +73,24 @@ class AllResourceProcess(models.Model):
db_table = 'all_resource_process'
verbose_name = '全资源监控'
verbose_name_plural = verbose_name
class TiktokUserVideos(models.Model):
"""TikTok用户视频信息"""
sec_user_id = models.CharField(max_length=255, unique=True)
nickname = models.CharField(max_length=255)
signature = models.TextField(blank=True, null=True)
follower_count = models.IntegerField(default=0)
total_favorited = models.IntegerField(default=0)
avatar_url = models.TextField(blank=True, null=True)
videos_folder = models.CharField(max_length=255)
video_paths = models.TextField(blank=True, null=True)
create_time = models.DateTimeField(auto_now_add=True)
update_time = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'TikTok用户视频'
verbose_name_plural = 'TikTok用户视频'
def __str__(self):
return f"{self.nickname} ({self.sec_user_id})"

View File

@ -15,4 +15,6 @@ urlpatterns = [
path('status/<int:pid>/', views.get_process_status, name='get_process_status'),
path('stop-directory-monitor/', views.stop_directory_monitor, name='stop_directory_monitor'),
path('directory-status/', views.get_directory_status, name='directory_status'),
path('tiktok/user-videos/', views.get_tiktok_user_videos, name='get-tiktok-user-videos'),
path('api/tiktok/fetch_videos/', views.fetch_tiktok_videos, name='fetch_tiktok_videos'),
]

View File

@ -2,7 +2,7 @@ from django.http import JsonResponse
from .tasks import monitor_process, get_process_gpu_usage
import threading
import psutil
from .models import HighCPUProcess, HighGPUProcess, HighMemoryProcess, AllResourceProcess
from .models import HighCPUProcess, HighGPUProcess, HighMemoryProcess, AllResourceProcess, TiktokUserVideos
import logging
import os
from datetime import datetime
@ -16,10 +16,9 @@ import pytz
from pathlib import Path # 使用 pathlib 处理跨平台路径
import json
from django.conf import settings
import requests
import threading
directory_monitoring = {}
# 全局变量来控制检测线程
@ -30,6 +29,12 @@ is_monitoring = False
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LOG_DIR = os.path.join(BASE_DIR, 'logs', 'process_monitor')
# 创建保存视频的基本路径
TIKTOK_VIDEOS_PATH = os.path.join(BASE_DIR, 'media', 'tiktok_videos')
# 确保基本目录存在
os.makedirs(TIKTOK_VIDEOS_PATH, exist_ok=True)
# 确保基础目录结构存在,添加 'all' 目录
for resource_type in ['cpu', 'memory', 'gpu', 'all']:
os.makedirs(os.path.join(LOG_DIR, resource_type), exist_ok=True)
@ -40,6 +45,9 @@ logger = logging.getLogger('monitor')
# 全局变量来跟踪监控的目录
monitored_directories = set()
# 在文件顶部添加 API 基础 URL
API_BASE_URL = "http://81.69.223.133:45268"
# 添加新的监控线程函数
def monitor_directory(directory):
"""持续监控目录的后台任务"""
@ -1012,3 +1020,342 @@ def get_open_files(proc):
return [f.path for f in proc.open_files()]
except:
return []
@csrf_exempt
@require_http_methods(["POST"])
def fetch_tiktok_videos(request):
"""获取TikTok用户视频并下载播放量前十的视频"""
try:
data = json.loads(request.body)
unique_id = data.get('unique_id')
if not unique_id:
return JsonResponse({
'status': 'error',
'message': '请提供TikTok用户ID(unique_id)'
}, json_dumps_params={'ensure_ascii': False})
# 调用API获取用户资料和secUid
logger.info(f"正在获取用户 {unique_id} 的资料...")
user_profile = fetch_user_profile(unique_id)
if not user_profile or 'data' not in user_profile:
return JsonResponse({
'status': 'error',
'message': f'无法获取用户 {unique_id} 的资料'
}, json_dumps_params={'ensure_ascii': False})
# 从API响应中提取secUid和其他用户信息
try:
user_info = user_profile['data']['userInfo']['user']
sec_uid = user_info['secUid']
# 提取其他用户信息
nickname = user_info.get('nickname', f'用户_{unique_id}')
signature = user_info.get('signature', '')
avatar_url = user_info.get('avatarLarger', '')
user_stats = user_profile['data']['userInfo'].get('stats', {})
follower_count = user_stats.get('followerCount', 0)
heart_count = user_stats.get('heartCount', 0)
logger.info(f"成功获取用户secUid: {sec_uid}")
except (KeyError, TypeError) as e:
logger.error(f"解析用户资料出错: {e}")
return JsonResponse({
'status': 'error',
'message': f'解析用户资料出错: {str(e)}'
}, json_dumps_params={'ensure_ascii': False})
# 确保用户目录存在
user_dir = os.path.join(TIKTOK_VIDEOS_PATH, unique_id)
os.makedirs(user_dir, exist_ok=True)
# 获取用户视频
videos_data = fetch_user_videos(sec_uid)
# 提取视频信息并按播放量排序
all_videos = []
if videos_data and isinstance(videos_data, dict) and 'data' in videos_data and 'itemList' in videos_data['data']:
for video in videos_data['data']['itemList']:
try:
# 确保提取正确的视频ID
video_id = video.get('id', '')
if not video_id or not str(video_id).isdigit():
logger.warning(f"跳过无效的视频ID: {video_id}")
continue
# 安全地获取统计数据
stats = video.get('stats', {})
if not isinstance(stats, dict):
stats = {}
play_count = int(stats.get('playCount', 0))
# 收集视频信息
all_videos.append({
'id': video_id,
'desc': video.get('desc', ''),
'create_time': video.get('createTime', 0),
'cover': video.get('cover', ''),
'play_count': play_count,
'comment_count': int(stats.get('commentCount', 0)),
'digg_count': int(stats.get('diggCount', 0)),
'share_count': int(stats.get('shareCount', 0))
})
except Exception as e:
logger.error(f"处理视频数据出错: {str(e)}")
continue
# 按播放量排序并获取前10个
all_videos.sort(key=lambda x: x['play_count'], reverse=True)
top_videos = all_videos[:10]
# 下载视频
downloaded_videos = []
for i, video in enumerate(top_videos):
# 获取视频ID
video_id = video['id'] # 这是数字ID
# 构建保存路径
save_path = os.path.join(user_dir, f"{video_id}.mp4")
logger.info(f"开始下载第 {i+1} 个热门视频(ID: {video_id}): {video['desc'][:20]}...")
# 使用正确的ID调用下载函数
if download_video(video_id, unique_id, save_path):
video['download_path'] = save_path
downloaded_videos.append(video)
logger.info(f"下载成功: {save_path}")
else:
logger.error(f"下载失败: {video_id}")
# 避免频繁请求被限制
time.sleep(1)
# 保存用户信息和视频信息到数据库
video_info_json = json.dumps([{
'id': v['id'],
'desc': v['desc'],
'play_count': v['play_count']
} for v in downloaded_videos], ensure_ascii=False)
user_record = TiktokUserVideos.objects.update_or_create(
sec_user_id=sec_uid,
defaults={
'nickname': nickname,
'signature': signature,
'follower_count': follower_count,
'total_favorited': heart_count,
'avatar_url': avatar_url,
'videos_folder': user_dir,
'video_paths': video_info_json
}
)
return JsonResponse({
'status': 'success',
'message': '处理完成',
'user_info': {
'nickname': nickname,
'unique_id': unique_id,
'sec_uid': sec_uid,
'avatar': avatar_url,
'follower_count': follower_count,
'total_favorited': heart_count,
'signature': signature
},
'videos_count': len(videos_data['data']['itemList']) if videos_data and 'data' in videos_data and 'itemList' in videos_data['data'] else 0,
'downloaded_videos': len(downloaded_videos),
'videos': [{'id': v['id'], 'desc': v['desc'][:50], 'play_count': v['play_count']} for v in downloaded_videos],
'video_directory': user_dir # 添加视频目录路径方便查找
}, json_dumps_params={'ensure_ascii': False})
except Exception as e:
logger.error(f"处理TikTok视频失败: {e}")
import traceback
logger.error(f"详细错误: {traceback.format_exc()}")
return JsonResponse({
'status': 'error',
'message': f'处理TikTok视频失败: {str(e)}'
}, json_dumps_params={'ensure_ascii': False})
@require_http_methods(["GET"])
def get_tiktok_user_videos(request):
"""获取已下载的TikTok用户视频列表"""
try:
sec_user_id = request.GET.get('sec_user_id')
if not sec_user_id:
# 如果没有指定用户ID返回所有用户列表
users = TiktokUserVideos.objects.all().values('sec_user_id', 'nickname', 'follower_count', 'videos_folder', 'create_time')
return JsonResponse({
'status': 'success',
'users': list(users)
}, json_dumps_params={'ensure_ascii': False})
# 查询指定用户信息
try:
user = TiktokUserVideos.objects.get(sec_user_id=sec_user_id)
# 解析视频信息JSON
video_info = json.loads(user.video_paths) if user.video_paths else []
# 获取文件夹中的文件列表
videos_folder = user.videos_folder
video_files = []
if os.path.exists(videos_folder):
video_files = [f for f in os.listdir(videos_folder) if os.path.isfile(os.path.join(videos_folder, f))]
except TiktokUserVideos.DoesNotExist:
return JsonResponse({
'status': 'error',
'message': f'用户 {sec_user_id} 不存在'
}, json_dumps_params={'ensure_ascii': False})
return JsonResponse({
'status': 'success',
'user_info': {
'sec_user_id': user.sec_user_id,
'nickname': user.nickname,
'signature': user.signature,
'follower_count': user.follower_count,
'total_favorited': user.total_favorited,
'avatar_url': user.avatar_url,
'create_time': user.create_time.strftime('%Y-%m-%d %H:%M:%S'),
'update_time': user.update_time.strftime('%Y-%m-%d %H:%M:%S')
},
'videos_folder': videos_folder,
'video_files': video_files,
'video_info': video_info
}, json_dumps_params={'ensure_ascii': False})
except Exception as e:
logger.error(f"获取TikTok视频列表失败: {str(e)}")
return JsonResponse({
'status': 'error',
'message': f'获取TikTok视频列表失败: {str(e)}'
}, json_dumps_params={'ensure_ascii': False})
# 辅助函数
def fetch_user_videos(sec_uid, max_cursor=0, count=20):
"""获取用户视频列表"""
url = f"{API_BASE_URL}/api/tiktok/web/fetch_user_post?secUid={sec_uid}"
try:
response = requests.get(url)
if response.status_code == 200:
return response.json()
else:
logger.error(f"获取视频列表失败: {response.status_code}")
return None
except Exception as e:
logger.error(f"获取视频列表异常: {e}")
return None
def fetch_user_profile(unique_id):
"""获取用户基本信息"""
# 添加User-Agent头模拟浏览器访问
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Referer': 'https://www.tiktok.com/'
}
url = f"{API_BASE_URL}/api/tiktok/web/fetch_user_profile?uniqueId={unique_id}"
try:
logger.info(f"正在请求用户资料: {url}")
# 添加重试机制
for attempt in range(3):
try:
response = requests.get(url, headers=headers, timeout=30)
break
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
logger.warning(f"请求超时或连接错误,尝试第{attempt+1}次重试: {e}")
if attempt == 2: # 最后一次重试失败
raise
time.sleep(2) # 等待2秒后重试
if response.status_code == 200:
try:
data = response.json()
logger.info(f"获取用户资料成功: {unique_id}")
# 打印完整响应以便调试
logger.info(f"API原始响应: {data}")
# 验证数据完整性
if 'data' not in data or not data['data']:
logger.error(f"API响应缺少data字段: {data}")
return None
if 'userInfo' not in data['data'] or not data['data']['userInfo']:
logger.error(f"API响应缺少userInfo字段: {data['data']}")
return None
if 'user' not in data['data']['userInfo'] or not data['data']['userInfo']['user']:
logger.error(f"API响应缺少user字段: {data['data']['userInfo']}")
return None
# 打印用户信息
logger.info(f"用户信息: {data['data']['userInfo']['user']}")
return data
except json.JSONDecodeError:
logger.error(f"API响应不是有效的JSON: {response.text[:500]}")
return None
else:
logger.error(f"获取用户信息失败: HTTP {response.status_code}, 响应: {response.text[:500]}")
return None
except Exception as e:
logger.error(f"获取用户信息异常: {e}")
return None
def download_video(video_id, unique_id, save_path):
"""使用API的直接下载接口下载TikTok视频"""
# 确保视频ID是纯数字
if not str(video_id).isdigit():
logger.error(f"无效的视频ID: {video_id},必须是纯数字")
return False
# 构建标准TikTok视频URL
tiktok_url = f"https://www.tiktok.com/@{unique_id}/video/{video_id}"
logger.info(f"构建的TikTok URL: {tiktok_url}")
# 构建完整的API请求URL
api_url = f"http://81.69.223.133:45268/api/download"
full_url = f"{api_url}?url={tiktok_url}&prefix=true&with_watermark=false"
logger.info(f"完整的API请求URL: {full_url}")
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
}
try:
# 直接使用完整URL发送请求
response = requests.get(full_url, headers=headers, stream=True, timeout=60)
# 检查响应状态
if response.status_code != 200:
logger.error(f"下载视频失败: {response.status_code} - {response.text[:200] if response.text else '无响应内容'}")
return False
# 获取内容类型
content_type = response.headers.get('Content-Type', '')
logger.info(f"响应内容类型: {content_type}")
# 保存文件
with open(save_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
file_size = os.path.getsize(save_path)
logger.info(f"视频已下载到: {save_path},文件大小: {file_size}字节")
return True
except Exception as e:
logger.error(f"下载视频异常: {e}")
import traceback
logger.error(f"详细错误: {traceback.format_exc()}")
return False

BIN
response.json Normal file

Binary file not shown.