HotGuard插件页面

HeatRecommend插件页面
This commit is contained in:
userA
2023-10-09 22:44:23 +08:00
parent 1aac91f567
commit eb2a325eef
33 changed files with 1059 additions and 283 deletions

View File

@@ -31,6 +31,7 @@ public class BarrageEvent {
private String date;
private String fileName;
private String suffix=".flv";
private List<Barrage> barrages;
private boolean isSort = false;

View File

@@ -11,6 +11,7 @@ import org.example.core.bgevnet.bgscore.BarragePoint;
import org.example.core.bgevnet.bgscore.BarrageScoreCurvePlugin;
import org.example.core.section.SectionRequest;
import org.example.core.section.VideoSectionWorkShop;
import org.example.init.InitPluginRegister;
import org.example.plugin.PluginCheckAndDo;
import org.example.plugin.SpringBootPlugin;
import org.example.bean.Barrage;
@@ -83,6 +84,7 @@ public class BarrageEventCenter extends SpringBootPlugin {
return ((BarragePopularRangePlugin) plugin).findRange(points);
}, PluginName.BARRAGE_POPULAR_RANGE_PLUGIN, List.class
);
if(popularRanges!=null){
PluginCheckAndDo.CheckAndDo(plugin -> {
for (PopularRange popularRange : popularRanges) {
@@ -100,5 +102,6 @@ public class BarrageEventCenter extends SpringBootPlugin {
}
//TODO 弹幕标签插件
}
}
}

View File

@@ -19,7 +19,7 @@ public class BarrageUtil {
List<Barrage> list = ((JSONArray) barrages).toJavaList(Barrage.class);
list = list.stream().sorted(Comparator.comparing(Barrage::getTimeReal)).collect(Collectors.toList());
if(list.isEmpty()){
return false;
JsonFileUtil.writeJsonFile(outputFilePath,Map.of("data",list,"updateTime",TimeUtil.getNowTime_YMDHMS()));
}else{
long startTime = list.get(0).getTimeReal() + start;
long endTime = list.get(0).getTimeReal() + end;

View File

@@ -15,14 +15,15 @@ import java.util.concurrent.*;
/**
* ChopperBot系统中专门用来处理异步事件的类
*/
public class OddJobBoy implements ChopperBotGuardianTask {
public class OddJobBoy {
private static volatile OddJobBoy Instance;
private BlockingQueue<OddJob> oddjobs;
private ExecutorService pool;
private OddJobBoy(){
oddjobs = new ArrayBlockingQueue<>(1024);
pool = Executors.newCachedThreadPool(new NamedThreadFactory("oddjob"));
}
public static OddJobBoy Boy(){
@@ -37,20 +38,6 @@ public class OddJobBoy implements ChopperBotGuardianTask {
}
public void addWork(OddJob job) throws InterruptedException {
oddjobs.put(job);
}
@Override
public void threadTask() {
while(true){
try {
OddJob job = oddjobs.take();
ChopperLogFactory.getLogger(LoggerType.System).info("<OddJobBoy> boy get a odd job:{},Processing...",job);
job.doJob();
}catch (InterruptedException e){
}
}
pool.submit(job::doJob);
}
}

View File

@@ -81,7 +81,7 @@ public class HotController {
@CheckPlugin(needPlugin = {PluginName.HOT_GUARD_PLUGIN})
@GetMapping("/hotGuard/guard")
public Result hotGuards(){
List<Guard> guards = hotModuleService.hotModuleGuardApi().getGuards();
List<GuardVO> guards = hotModuleService.hotModuleGuardApi().getGuards();
return Result.success(
Map.of("list",guards)
);
@@ -126,10 +126,15 @@ public class HotController {
@CheckPlugin(needPlugin = {PluginName.HOT_RECOMMENDATION_PLUGIN})
@PostMapping("/hotRecommendation/add")
public Result addFollowDogs(@RequestBody FollowDog dog){
dog.setId(null);
boolean success = hotModuleService.heatRecommendApi().addFollowDog(dog);
return Result.success(
Map.of("success",success)
);
if(success){
return Result.success(
Map.of("add",dog)
);
}else{
return Result.error("403","添加失败");
}
}
@CheckPlugin(needPlugin = {PluginName.HOT_RECOMMENDATION_PLUGIN})

View File

@@ -27,7 +27,6 @@ public class WorldInitMachine extends ModuleInitMachine{
@Override
public boolean init() {
ChopperBotGuardPool.init();
OddJobBoy.Boy().guardian();
try {
initMachines = PluginUtil.getAllModuleInit();
return initLogger(()->{

View File

@@ -7,6 +7,7 @@ import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.UUID;
/**
* @author Genius
@@ -22,6 +23,7 @@ public class HeatRecommendApi {
HeatRecommendation heatRecommendation;
public boolean addFollowDog(FollowDog dog){
dog.setDogId(UUID.randomUUID().toString());
if (followDogService.addFollowDog(dog)) {
return heatRecommendation.addFollowDog(dog);
}

View File

@@ -1,13 +1,16 @@
package org.example.api;
import org.example.bean.GuardVO;
import org.example.bean.HotModuleSetting;
import org.example.core.guard.Guard;
import org.example.core.guard.HotModuleGuard;
import org.example.service.HotModuleSettingService;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Genius
@@ -33,7 +36,11 @@ public class HotModuleGuardApi {
return false;
}
public List<Guard> getGuards(){
return hotModuleGuard.getGuards();
public List<GuardVO> getGuards(){
return hotModuleGuard.getGuards().stream().map(guard->{
GuardVO guardVO = new GuardVO();
BeanUtils.copyProperties(guard,guardVO);
return guardVO;
}).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,21 @@
package org.example.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* @author Genius
* @date 2023/10/09 16:03
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GuardVO {
private String guardName;
private long delayTime;
private int failRetryTimes;
private LocalDateTime preGuardTime;
}

View File

@@ -13,6 +13,7 @@ import org.example.log.ResultLogger;
import org.example.plugin.PluginCheckAndDo;
import org.slf4j.Logger;
import java.time.LocalDateTime;
import java.util.List;
/**
@@ -34,11 +35,14 @@ public class Guard<T extends HotModuleLoadTask> implements Runnable, ResultLogge
private long delayTime;
private int failRetryTimes;
private LocalDateTime preGuardTime;
@Override
public void run() {
int retryTimes = 0;
Object data = task.start();
preGuardTime = LocalDateTime.now();
if(task.isFinish() == HotModuleLoadTask.FinishFlag.FINISH){
successLog();
}else{

View File

@@ -85,7 +85,7 @@ public class HotModuleGuard extends SpringBootPlugin {
if(loadTask!=null){
addGuard(new Guard(this.logger,groupName,loadTask,
hotModuleSetting.getUpdateHotModuleTimes(),
hotModuleSetting.getFailRetryTimes()));
hotModuleSetting.getFailRetryTimes(),null));
}else{
this.error(String.format("Unable to listen %s hot module,cause: invalid loadTask!", groupName));
return false;
@@ -99,7 +99,7 @@ public class HotModuleGuard extends SpringBootPlugin {
if(loadTask!=null){
addGuard(new Guard(this.logger,groupName,loadTask,
hotModuleSetting.getUpdateHotLivesTimes(),
hotModuleSetting.getFailRetryTimes()));
hotModuleSetting.getFailRetryTimes(),null));
}else{
this.error(String.format("Unable to listen %s hot live,cause: invalid loadTask!", groupName));
return false;

View File

@@ -28,6 +28,7 @@ import org.example.plugin.SpringBootPlugin;
import org.example.service.FollowDogService;
import org.example.service.HotModuleSettingService;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.*;
@@ -146,25 +147,26 @@ public class HeatRecommendation extends SpringBootPlugin {
followDogs.add(dog);
return true;
}
return false;
return true;
}
public boolean removeFollowDog(String platform,String dogId){
List<FollowDog> followDogs = platformFollowDogMap.get(platform);
if(followDogs!=null){
return followDogs.removeIf(dog -> {
followDogs.removeIf(dog -> {
return dog.getDogId().equals(dogId);
});
}
return false;
return true;
}
public boolean updateFollowDog(FollowDog dog){
List<FollowDog> followDogs = platformFollowDogMap.get(dog.getPlatform());
if(followDogs!=null){
return followDogs.removeIf(dog1 -> {return dog1.getDogId().equals(dog.getDogId());})&&followDogs.add(dog);
followDogs.removeIf(dog1 -> {return dog1.getDogId().equals(dog.getDogId());});
followDogs.add(dog);
}
return false;
return true;
}
public boolean openPlatformFollowDog(String platform,boolean isOpen){
@@ -222,12 +224,12 @@ public class HeatRecommendation extends SpringBootPlugin {
}
private List<String> getBanList(String banLiver){
if(banLiver==null){
return new ArrayList<>();
}else{
if(StringUtils.hasText(banLiver)){
String s = banLiver.substring(1, banLiver.length() - 1);
String[] ss = s.split(",");
return List.of(ss);
}else{
return new ArrayList<>();
}
}
}

View File

@@ -36,7 +36,7 @@ public class FollowDogServiceImpl extends ServiceImpl<FollowDogMapper, FollowDog
@Override
public boolean addFollowDog(FollowDog dog) {
return mapper.insert(dog)==1;
return save(dog);
}
@Override

View File

@@ -39,23 +39,25 @@ public class VideoSectionWorkShop extends SpringGuardPlugin {
public void start() {
try {
SectionRequest request = requests.poll(1000, TimeUnit.SECONDS);
long videoStartTime = TimeUtil.getTimeNaos(request.getDate());
long start = (timeBias(request.getStartTime())-videoStartTime)/1000;
start = start<0?0:start;
long end = (timeBias(request.getEndTime())-videoStartTime)/1000;
if(request!=null){
long videoStartTime = TimeUtil.getTimeNaos(request.getDate());
long start = (timeBias(request.getStartTime())-videoStartTime)/1000;
start = start<0?0:start;
long end = (timeBias(request.getEndTime())-videoStartTime)/1000;
String root = Paths.get(liveRoot,request.getAction(),request.getPlatform()).toString();
String startTime = VideoUtil.formatTimeToFFMpeg(start);
String endTime = VideoUtil.formatTimeToFFMpeg(end);
String videoName = request.getVideoName();
String[] split = videoName.split("\\.");
String oldPath = Paths.get(root,videoName).toString();
String liver = request.getLiver();
String date = request.getDate();
String newVideoName = FileNameBuilder.buildVideoFileNameNoSuffix(liver, date)+"_section("+ FileUtil.convertTimeToFile(startTime) +"-"+FileUtil.convertTimeToFile(endTime)+")."+split[1];
String newPath = Paths.get(root,newVideoName).toString();
if (VideoUtil.cutVideoByFFMpeg(oldPath,newPath,startTime,endTime)) {
VideoSection videoSection = new VideoSection(newVideoName,request.getTag(),request.getLiver(),request.getPlatform());
String root = Paths.get(liveRoot,request.getAction(),request.getPlatform()).toString();
String startTime = VideoUtil.formatTimeToFFMpeg(start);
String endTime = VideoUtil.formatTimeToFFMpeg(end);
String videoName = request.getVideoName();
String[] split = videoName.split("\\.");
String oldPath = Paths.get(root,videoName).toString();
String liver = request.getLiver();
String date = request.getDate();
String newVideoName = FileNameBuilder.buildVideoFileNameNoSuffix(liver, date)+"_section("+ FileUtil.convertTimeToFile(startTime) +"-"+FileUtil.convertTimeToFile(endTime)+")."+split[1];
String newPath = Paths.get(root,newVideoName).toString();
if (VideoUtil.cutVideoByFFMpeg(oldPath,newPath,startTime,endTime)) {
VideoSection videoSection = new VideoSection(newVideoName,request.getTag(),request.getLiver(),request.getPlatform());
}
}
}catch (InterruptedException e){
return;

View File

@@ -11,6 +11,7 @@ import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.*;
import org.bytedeco.javacv.Frame;
import org.example.constpool.ConstPool;
import org.example.thread.oddjob.OddJobBoy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ws.schild.jave.Encoder;
@@ -20,8 +21,10 @@ import ws.schild.jave.MultimediaObject;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.MessageFormat;
@@ -416,17 +419,44 @@ public class VideoUtil {
}
public static boolean cutVideoByFFMpeg(String inputFilePath,String outputFilePath,String start,String end){
long videoTime = getVideoTime(inputFilePath);
System.out.println(inputFilePath+"视频时长为:"+videoTime+" "+formatTimeToFFMpeg(videoTime));
try {
String ffmpegCmd = FFMPEG_PATH + " -i " + "\""+inputFilePath+"\"" + " -ss " + start + " -to " + end + " -c copy " + "\""+outputFilePath+"\"" +" -y";
System.out.println(ffmpegCmd);
// 执行FFmpeg命令
Process process = Runtime.getRuntime().exec(ffmpegCmd);
// 等待命令执行完成
int exitCode = process.waitFor();
OddJobBoy.Boy().addWork(()->{
BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
try {
while ((in.readLine()) != null) {}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
OddJobBoy.Boy().addWork(()->{
BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()));
try {
while ((err.readLine()) != null) {}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
err.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
int exitCode = process.waitFor();
if (exitCode == 0) {
System.out.printf("in:%s 视频分割完成。 out:%s\n",inputFilePath, outputFilePath);
return true;

View File

@@ -0,0 +1,35 @@
package org.example.barrage;
import org.example.ConsoleApplication;
import org.example.constpool.PluginName;
import org.example.core.bgevnet.BarrageEvent;
import org.example.core.bgevnet.bghot.BarragePopularRangePlugin;
import org.example.core.bgevnet.bghot.PopularRange;
import org.example.core.bgevnet.bgscore.BarragePoint;
import org.example.core.bgevnet.bgscore.BarrageScoreCurvePlugin;
import org.example.init.InitPluginRegister;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
/**
* @author Genius
* @date 2023/10/09 00:27
**/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ConsoleApplication.class,webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class BarragePoupularRangeTest {
@Test
public void Test(){
BarrageEvent event = new BarrageEvent("douyu","online",null,null,"即将拥有人鱼线的PDD_2023-09-14 14_37_54.json");
List<BarragePoint> points = InitPluginRegister.getPlugin(PluginName.BARRAGE_SCORE_CURVE_PLUGIN, BarrageScoreCurvePlugin.class).generateCurve(event);
List<PopularRange> ranges = InitPluginRegister.getPlugin(PluginName.BARRAGE_POPULAR_RANGE_PLUGIN, BarragePopularRangePlugin.class).findRange(points);
System.out.println(ranges);
}
}

View File

@@ -24,12 +24,12 @@
"HotRecommendation":true,
"BarrageScoreCurve":true,
"FileCacheManager":true,
"LiverFollower":true,
"LiverFollower":false,
"TaskMonitor":true,
"HotConfig":true,
"TaskCenter":true,
"CreeperConfig":true
}
},
"updateTime":"2023-10-07 23:05:43"
"updateTime":"2023-10-09 22:41:45"
}

View File

@@ -1,4 +1,5 @@
import request from '@/utils/request';
import {FollowDog} from "@/views/app/hot/heat_recommend/FollowDogTypes";
export function openFollowDog(platform:string,isOpen:boolean) {
return request({
@@ -10,3 +11,29 @@ export function openFollowDog(platform:string,isOpen:boolean) {
}
});
}
export function getFollowDogs(){
return request({
url: '/hot/hotRecommendation/list',
method: 'get',
});
}
export function addFollowDog(dog:FollowDog){
return request.post("/hot/hotRecommendation/add",dog)
}
export function updateFollowDog(dog:FollowDog){
return request.post("/hot/hotRecommendation/update",dog)
}
export function deleteFollowDog(dogId:string,platform:string){
return request({
url: '/hot/hotRecommendation/delete',
method: 'get',
params: {
dogId,
platform
}
});
}

View File

@@ -14,7 +14,7 @@ export function updateHotGuardSetting(setting:Setting) {
return request.post('/hot/hotGuard/update',setting)
}
export function guards() {
export function getGuards() {
return request({
url: '/hot/hotGuard/guard',
method: 'get',

View File

@@ -27,7 +27,7 @@ export default [
icon:"mdi mdi-bird",
key:"menu.heat_recommend",
text:"Heat Recommend",
link: "/apps/heat_recommend"
link: "/apps/heatRecommend"
},
{
icon:"mdi mdi-star-face",

View File

@@ -3,6 +3,7 @@ import todoRoutes from "@/views/app/todo/todoRoutes";
import emailRoutes from "@/views/app/email/emailRoutes";
import chatRoutes from "@/views/app/chat/chatRoutes";
import taskRoutes from "@/views/app/taskcenter/taskRoutes";
import hotGuardRoutes from "@/views/app/hot/hot_guard/hotGuardRoutes";
export default [
{
@@ -69,6 +70,7 @@ export default [
import(
/* webpackChunkName: "utility-board" */ "@/views/app/hot/hot_guard/HotGuardView.vue"
),
children: [...hotGuardRoutes],
meta: {
requiresAuth: true,
title: "Hot Guard",
@@ -76,6 +78,20 @@ export default [
category: "APP",
},
},
{
path: "/apps/heatRecommend",
name: "app-heat-recommend",
component: () =>
import(
/* webpackChunkName: "utility-board" */ "@/views/app/hot/heat_recommend/HeatRecommendationView.vue"
),
meta: {
requiresAuth: true,
title: "Heat Recommend",
layout: "ui",
category: "APP",
},
},
{
path: "/apps/email",
meta: {

View File

@@ -6,3 +6,18 @@ export const nanosToHMS_CN = (milliseconds: number) => {
return `${hours}小时${minutes}分钟${seconds}`;
}
export function isoStrToNormal(isoString: string): string {
const date = new Date(isoString);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
}

View File

@@ -0,0 +1,8 @@
export interface FollowDog{
id:number;
dogId:string;
platform:string;
moduleName:string;
top:number;
banLiver:string;
}

View File

@@ -0,0 +1,385 @@
<template>
<!-- board column -->
<v-row style="min-width: 1000px">
<v-col
cols="4"
v-for="column in columns"
:key="column.platform"
class="pa-3 flex-1"
>
<div class="d-flex align-center">
<h5 class="font-weight-bold">{{ column.platform }}</h5>
<v-spacer></v-spacer>
<!-- add new card button -->
<v-btn
variant="text"
rounded
icon="mdi-plus"
size="small"
color="primary"
class="mr-n3"
@click="column.isAddVisible = !column.isAddVisible"
>
</v-btn>
</div>
<!-- add new card form -->
<v-card v-show="column.isAddVisible" class="pa-5 my-2">
<v-text-field
color="primary"
v-model="column.platform"
label="平台"
variant="underlined"
hideDetails
disabled
placeholder="Input title for this card"
autofocus
></v-text-field>
<v-text-field
color="primary"
v-model="column.addCard.moduleName"
label="类型"
variant="underlined"
hideDetails
placeholder="输入你要推荐的直播类型(例如:英雄联盟,以平台类型为基准)"
autofocus
></v-text-field>
<v-text-field
color="primary"
v-model="column.addCard.top"
label="Top"
variant="underlined"
hideDetails
placeholder="输入该推荐该类型的前几个直播"
autofocus
></v-text-field>
<v-text-field
color="primary"
v-model="column.addCard.banLiver"
label="Ban"
variant="underlined"
hideDetails
placeholder="输入需要ban掉的主播,支持正则表达式"
autofocus
></v-text-field>
<div class="mt-3 d-flex flex-md-row flex-column">
<v-btn
class="flex-1 ma-1"
size="small"
@click="column.isAddVisible = !column.isAddVisible"
>Cancel</v-btn
>
<v-btn
class="flex-1 ma-1"
size="small"
color="primary"
@click="addCard(column)"
>Add</v-btn
>
</div>
</v-card>
<!-- draggable cards -->
<vue-draggable
v-model="column.cards"
v-bind="dragOptions"
class="list-group"
@change="column.callback"
itemKey="id"
>
<template #item="{ element, index }">
<follow-dog-card
:key="index"
:card="element"
class="board-item my-2 pa-2"
@edit="showEdit(element)"
@delete="showDelete(element)"
/>
</template>
</vue-draggable>
</v-col>
</v-row>
<!-- edit card dialog -->
<v-dialog persistent v-model="editDialog" width="600">
<v-card>
<v-card-title class="pa-4 d-flex align-center">
<span class="flex-1">Edit FollowDog 🐶</span>
<v-btn
variant="text"
rounded
icon="mdi-close"
size="small"
color="primary"
class="mr-n3"
@click="editDialog = false"
>
</v-btn>
</v-card-title>
<v-divider></v-divider>
<div class="pa-4">
<v-text-field
class="py-2 px-1"
color="primary"
v-model="cardToEdit.platform"
label="平台"
variant="plain"
hideDetails
placeholder="输入平台"
autofocus
disabled
></v-text-field>
<v-divider></v-divider>
<v-text-field
class="py-2 px-1"
color="primary"
v-model="cardToEdit.moduleName"
label="类型"
variant="plain"
hideDetails
placeholder="输入你要推荐的直播类型(例如:英雄联盟,以平台类型为基准)"
autofocus
></v-text-field>
<v-divider></v-divider>
<v-text-field
class="py-2 px-1"
color="primary"
v-model="cardToEdit.top"
label="Top"
variant="plain"
hideDetails
placeholder="输入该推荐该类型的前几个直播"
autofocus
></v-text-field>
<v-divider></v-divider>
<v-text-field
class="py-2 px-1"
color="primary"
v-model="cardToEdit.banLiver"
label="Ban"
variant="plain"
hideDetails
placeholder="输入需要ban掉的主播,支持正则表达式"
autofocus
></v-text-field>
</div>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<v-btn variant="outlined" @click="editDialog = false">Cancel</v-btn>
<v-spacer></v-spacer>
<v-btn variant="flat" color="primary" @click="saveCard">Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- delete card dialog -->
<v-dialog v-model="deleteDialog" max-width="600">
<v-card>
<v-card-title class="text-headline">Delete</v-card-title>
<v-card-text>确定要删除{{cardToDelete.dogId}}?</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn variant="plain" color="primary" @click="deleteDialog = false"
>Cancel</v-btn
>
<v-btn variant="flat" color="error" @click="deleteCard()">Delete</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import VueDraggable from "vuedraggable";
import FollowDogCard from "@/views/app/hot/heat_recommend/compoments/FollowDogCard.vue";
import {getAllConfigModule,getAllConfigFiles} from "@/api/FileController";
import {addFollowDog, deleteFollowDog, getFollowDogs, updateFollowDog} from "@/api/hot/heatRecommendApi";
import {useHotRecommendStore} from "@/views/app/hot/heat_recommend/heatRecommendationStore";
import {FollowDog} from "@/views/app/hot/heat_recommend/FollowDogTypes";
import {useSnackbarStore} from "@/stores/snackbarStore";
const hotRecommendStore = useHotRecommendStore()
const snackbarStore = useSnackbarStore();
const dragOptions = computed(() => {
return {
animation: 200,
group: "loadTask",
disabled: false,
ghostClass: "ghost",
};
});
const platforms = hotRecommendStore.platforms
const default_card = ref<FollowDog>({
id:0,
dogId:'',
platform: '',
moduleName: 'all',
top: 5,
banLiver:'',
})
type Column = {
platform: string,
cards: FollowDog[],
isAddVisible: false,
callback: Function,
addCard: FollowDog
}
const columns = ref<Column[]>([]);
const cards = ref<FollowDog[]>([]);
const deleteDialog = ref(false);
const cardToDelete = ref<FollowDog>();
const cardToEdit = ref<FollowDog>();
const title = ref("");
const description = ref("");
const editDialog = ref(false);
onMounted(async () => {
await initFollowDog();
initColumns();
cards.value = hotRecommendStore.followDogs;
parseCards(hotRecommendStore.followDogs);
});
// Init 模块类型
const initFollowDog = async()=>{
await getFollowDogs().then(res=>{
hotRecommendStore.followDogs = res.data['list'];
})
}
// Init 配置文件
const initColumns = () => {
platforms.forEach((state, index) => {
columns.value.push({
platform: state,
cards: [],
isAddVisible: false,
callback: (e) => changeState(e, index),
addCard: Object.assign({},default_card.value),
} );
});
};
const parseCards = (cards) => {
if (!cards) return columns.value.map((column) => (column.cards = []));
columns.value.forEach((column) => {
column.cards = cards
.filter((card) => card.platform === column.platform)
.sort((a, b) => (a.order < b.order ? -1 : 0));
});
};
// Add
const addCard = async (column) => {
if (!column.addCard) return;
column.addCard.platform = column.platform
await addFollowDog(column.addCard).then(res=>{
if(res.code==='200'){
column.cards.push(res.data["add"])
hotRecommendStore.addFollowDog(res.data["add"])
snackbarStore.showSuccessMessage("添加成功")
}else{
snackbarStore.showErrorMessage("添加失败")
}
})
column.addCard = Object.assign({},default_card.value)
column.isAddVisible = false
};
const changeState = async (e, colIndex) => {
if (e.added || e.moved) {
const column = columns.value[colIndex];
const state = column.platform;
for (let i = 0; i < column.cards.length; i++) {
column.cards[i].order = i;
if(column.cards[i].platform!=state){
column.cards[i].platform = state;
await updateFollowDog(column.cards[i]).then(res=>{
if(res?.data['success']){
hotRecommendStore.updateFollowDog(column.cards[i])
snackbarStore.showSuccessMessage("更新成功")
}else{
snackbarStore.showErrorMessage("更新失败")
}
})
}
}
}
};
// Edit
const showEdit = (card) => {
cardToEdit.value = Object.assign({},card);
editDialog.value = true;
};
const saveCard = async () => {
if(cardToEdit.value!=null){
await updateFollowDog(cardToEdit.value).then(res=>{
if(res?.data['success']){
const column = columns.value.find(column=>column.platform===cardToEdit.value.platform)
if(column!=null){
const old = column.cards.find(dog=>dog.dogId===cardToEdit.value.dogId)
column.cards.splice(column.cards.indexOf(old),1,cardToEdit.value)
}
hotRecommendStore.updateFollowDog(cardToEdit.value)
snackbarStore.showSuccessMessage("更新成功")
}else{
snackbarStore.showErrorMessage("更新失败")
}
})
}
editDialog.value = false;
};
// Delete
const showDelete = (card) => {
cardToDelete.value = card;
deleteDialog.value = true;
};
const deleteCard = async () => {
deleteDialog.value = false;
await deleteFollowDog(cardToDelete.value.dogId,cardToDelete.value.platform).then(res=>{
if(res?.data['success']){
hotRecommendStore.deleteFollowDog(cardToDelete.value)
columns.value.forEach((column) => {
if(column.platform===cardToDelete.value.platform){
column.cards = column.cards
.filter((card) => cardToDelete.value.dogId !== card.dogId)
}
});
snackbarStore.showSuccessMessage("删除成功")
}else{
snackbarStore.showErrorMessage("删除失败")
}
})
};
watch(hotRecommendStore.followDogs, (newCards) => {
parseCards(newCards);
});
</script>
<style lang="scss" scoped>
.ghost {
opacity: 0.5;
background: #c8ebfb;
}
.board-item {
transition: transform 0.2s;
&:hover {
transition: transform 0.2s;
transform: translateY(-3px);
}
}
.list-group {
min-height: 100%;
}
</style>

View File

@@ -0,0 +1,67 @@
<template>
<v-card @click="$emit('edit')" class="pa-5 mt-4 card-shadow">
<div class="d-flex align-start font-weight-bold text-title">
<span class="flex-1">🐶{{ props.card.platform }}-{{ props.card.moduleName }}</span>
<v-menu location="bottom end" transition="slide-x-transition">
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
size="small"
variant="text"
icon="mdi-dots-vertical"
rounded
color="primary"
class="my-n2"
></v-btn>
</template>
<v-list density="compact">
<v-list-item @click="$emit('edit')">
<v-list-item-title class="d-inline-flex align-center">
<Icon
icon="flat-color-icons:edit-image"
:rotate="2"
:horizontalFlip="true"
:verticalFlip="true"
class="mr-1"
/>
<span> Edit</span>
</v-list-item-title>
</v-list-item>
<v-list-item @click="$emit('delete')">
<v-list-item-title class="d-inline-flex align-center">
<Icon
icon="flat-color-icons:full-trash"
:rotate="2"
:horizontalFlip="true"
:verticalFlip="true"
:inline="true"
class="mr-1"
/>
Delete</v-list-item-title
>
</v-list-item>
</v-list>
</v-menu>
</div>
<div class="text-content">平台🌟: {{ props.card.platform }}</div>
<div class="text-content">类型🎪: {{ props.card.moduleName }}</div>
<div class="text-content">Top🏆: {{ props.card.top }}</div>
<div class="text-content">Ban🚫{{ !props.card.banLiver?"暂无":props.card.banLiver }}</div>
<div class="text-content">dogId🔖: {{ props.card.dogId }}</div>
</v-card>
</template>
<script setup lang="ts">
import { Icon } from "@iconify/vue";
import {FollowDog} from "@/views/app/hot/heat_recommend/FollowDogTypes";
const props = defineProps<{
// Card content to display
card: FollowDog
}>();
</script>
<style lang="scss" scoped>
.card-shadow {
box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px !important;
}
</style>

View File

@@ -0,0 +1,32 @@
import { defineStore } from "pinia";
import {useSnackbarStore} from "@/stores/snackbarStore";
import {FollowDog} from "@/views/app/hot/heat_recommend/FollowDogTypes";
export const useHotRecommendStore = defineStore({
id: "hotRecommend",
state:()=>({
followDogs: ref<FollowDog[]>([]),
platforms: ['douyu','huya','bilibili','douyin','tiktok','twitch'],
}),
getters:{
getFollowDogs(){
return this.followDogs
}
},
actions:{
deleteFollowDog(dog:FollowDog){
this.followDogs = this.followDogs.filter(oldDog=>oldDog.dogId!==dog.dogId)
},
addFollowDog(dog:FollowDog){
this.followDogs.push(dog)
},
updateFollowDog(dog:FollowDog){
let old = this.followDogs.find(oldDog=>dog.dogId===oldDog.dogId)
this.followDogs.splice(this.followDogs.indexOf(old), 1, dog);
}
}
})

View File

@@ -1,92 +1,26 @@
<script setup lang="ts">
import {getHotGuardSetting} from "@/api/hot/hotGuardApi";
import {openFollowDog} from "@/api/hot/heatRecommendApi";
import router from "@/router";
import {useHotGuardStore} from "@/views/app/hot/hot_guard/hotGuardStore";
import {nanosToHMS_CN} from "@/utils/timeUtils";
import {Setting} from "@/views/app/hot/hot_guard/hotSettingTypes";
import {useSnackbarStore} from "@/stores/snackbarStore";
const hotGuardStore = useHotGuardStore();
const hotGuardStore = useHotGuardStore()
const snackbarStore = useSnackbarStore();
const currentLabel = ref<string>("设置")
onMounted(async()=>{
await getHotGuardSetting().then(res=>{
hotGuardStore.settings = res.data['list'].map((setting: any)=>setting as Setting)
})
})
const timeRules = [
(v)=> !!v || "时间必须存在",
(v)=> v > 0 || "时间必须大于0",
(v)=> v >= 1000 || "时间必须大于1s",
]
if (router.currentRoute.value.path === '/apps/hotGuard/guards') {
// 当前路由路径是/apps/hotGuard/guards
currentLabel.value = '监控守卫'
} else {
// 当前路由路径不是/apps/hotGuard/guards
currentLabel.value = '设置'
}
const dialog = ref(false);
const search = ref("");
const refForm = ref<Setting>();
const editedItem = ref<Setting>();
const editedIndex = ref(-1);
const defaultItem = ref<Setting>({
id: 0,
platform:"",
failRetryTimes:0,
enableHotModule:false,
enableHotLive:false,
followDogEnable:false,
updateHotModuleTimes:0,
updateHotLivesTimes:0
watch(currentLabel, (newLabel, oldLabel) => {
if(newLabel==='监控守卫'){
router.push('/apps/hotGuard/guards')
}else if(newLabel==='设置'){
router.push('/apps/hotGuard/settings')
}
});
//Methods
function saveBtnDisable(item: any){
const setting = item as Setting
return timeRules.some((rule)=>typeof rule(setting.updateHotModuleTimes) === "string")||
timeRules.some((rule)=>typeof rule(setting.updateHotLivesTimes) === "string")
}
function editItem(item: Setting,index: number) {
editedIndex.value = index;
editedItem.value = Object.assign({}, item);
dialog.value = true;
}
function updateSwitch(item: Setting){
hotGuardStore.updateSettingNoIndex(item)
}
function updateFollowDogSwitch(item: Setting){
openFollowDog(item.platform,!item.followDogEnable).then(res=>{
if(res?.data?.success){
if(item.followDogEnable){
snackbarStore.showSuccessMessage(item.platform+"跟风狗模式已打开!")
}else{
snackbarStore.showSuccessMessage(item.platform+"跟风狗模式已关闭!")
}
}else{
if(item.followDogEnable){
snackbarStore.showSuccessMessage(item.platform+"跟风狗模式打开失败!")
}else{
snackbarStore.showSuccessMessage(item.platform+"跟风狗模式已关闭失败!")
}
item.followDogEnable = !item.followDogEnable
}
})
}
function close() {
dialog.value = false;
editedItem.value = Object.assign({}, defaultItem.value);
editedIndex.value = -1;
}
function save() {
const setting = Object.assign({}, editedItem.value)
hotGuardStore.updateSetting(editedIndex.value,setting)
close();
}
//Computed Property
</script>
<template>
@@ -95,150 +29,30 @@ function save() {
<v-card-text>
<v-row>
<v-col cols="12" lg="4" md="6">
<v-text-field
density="compact"
v-model="search"
label="Search Contacts"
hide-details
variant="outlined"
color="primary"
></v-text-field>
</v-col>
<v-col cols="12" lg="8" md="6" class="text-right">
<v-dialog v-model="dialog" max-width="700">
<v-card>
<v-card-title class="pa-4 bg-secondary">
<span class="title text-white">Edit Setting</span>
</v-card-title>
<v-card-text>
<v-form
class="mt-5"
ref="form"
v-model="refForm"
lazy-validation
>
<v-row>
<v-col cols="12" sm="6">
<v-text-field
variant="outlined"
color="primary"
density="compact"
:rules="timeRules"
v-model="editedItem.updateHotModuleTimes"
label="热门模块更新时间(毫秒)"
type="email"
></v-text-field>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
variant="outlined"
color="primary"
density="compact"
:rules="timeRules"
v-model="editedItem.updateHotLivesTimes"
label="热门直播更新时间(毫秒)"
type="phone"
></v-text-field>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn color="error" @click="close">Cancel</v-btn>
<v-btn
color="secondary"
:disabled="saveBtnDisable(editedItem)"
variant="flat"
@click="save"
>Save</v-btn
>
</v-card-actions>
</v-card>
</v-dialog>
<v-chip-group v-model="currentLabel" mandatory>
<v-chip
filter
label
variant="text"
color="primary"
v-for="label in hotGuardStore.fieldLabelList"
:key="label"
:value="label"
>
{{ label }}
</v-chip>
</v-chip-group>
</v-col>
</v-row>
</v-card-text>
</v-card>
<v-card class="mt-2">
<v-table class="mt-5">
<thead>
<tr>
<th class="text-subtitle-1 font-weight-semibold">Id</th>
<th class="text-subtitle-1 font-weight-semibold">直播平台</th>
<th class="text-subtitle-1 font-weight-semibold">热门模块监控</th>
<th class="text-subtitle-1 font-weight-semibold">热门直播监控</th>
<th class="text-subtitle-1 font-weight-semibold">跟风狗模式</th>
<th class="text-subtitle-1 font-weight-semibold">热门模块更新时间</th>
<th class="text-subtitle-1 font-weight-semibold">热门直播更新时间</th>
<th class="text-subtitle-1 font-weight-semibold">操作</th>
</tr>
</thead>
<tbody class="text-body-1">
<tr v-for="(item,index) in hotGuardStore.getSettings" :key="item.id">
<td class="font-weight-bold">{{ item.id }}</td>
<td>
<div class="d-flex align-center py-1">
<div class="ml-5">
<p class="font-weight-bold">{{ item.platform }}</p>
</div>
</div>
</td>
<td>
<v-switch class="flex-col"
size="small"
style="padding-right: 65%"
v-model="item.enableHotModule"
color="rgb(33,150,243)"
@click="updateSwitch(item)"
inset
></v-switch>
</td>
<td>
<v-switch class="flex-col"
style="padding-right: 65%"
size="small"
v-model="item.enableHotLive"
color="rgb(33,150,243)"
@click="updateSwitch(item)"
inset
></v-switch>
</td>
<td>
<v-switch class="flex-col"
style="padding-right: 65%"
size="small"
v-model="item.followDogEnable"
color="green"
@click="updateFollowDogSwitch(item)"
inset
></v-switch>
</td>
<td>{{nanosToHMS_CN(item.updateHotModuleTimes)}}</td>
<td>{{nanosToHMS_CN(item.updateHotLivesTimes)}}</td>
<td>
<div class="d-flex align-center">
<v-tooltip text="Edit">
<template v-slot:activator="{ props }">
<v-btn
color="blue"
icon
variant="text"
@click="editItem(item,index)"
v-bind="props"
>
<v-icon>mdi-pencil-outline</v-icon>
</v-btn>
</template>
</v-tooltip>
</div>
</td>
</tr>
</tbody>
</v-table>
<router-view v-slot="{ Component }">
<transition name="fade">
<component :is="Component" />
</transition>
</router-view>
</v-card>
</v-container>
</template>

View File

@@ -0,0 +1,22 @@
export default [
{
path: "",
redirect: "/apps/hotGuard/settings",
},
{
path: "settings",
name: "hot-guard-settings",
component: () =>
import(
/* webpackChunkName: "apps-todo-tasks" */ "@/views/app/hot/hot_guard/pages/HotSettingsPage.vue"
),
},
{
path: "guards",
name: "hot-guard-guards",
component: () =>
import(
/* webpackChunkName: "apps-todo-completed" */ "@/views/app/hot/hot_guard/pages/HotGuardsPage.vue"
),
},
];

View File

@@ -9,7 +9,12 @@ export const useHotGuardStore = defineStore({
id: "hotGuard",
state:()=>({
settings: ref<Setting[]>([]),
guards:[]
guards:[],
fieldLabelList: ref<string[]>([
"设置",
"监控守卫"
]),
}),
getters:{

View File

@@ -0,0 +1,64 @@
<template>
<v-table class="mt-5">
<thead>
<tr>
<th class="text-subtitle-1 font-weight-semibold">监控守卫名称</th>
<th class="text-subtitle-1 font-weight-semibold">轮询时间</th>
<th class="text-subtitle-1 font-weight-semibold">失败重试次数</th>
<th class="text-subtitle-1 font-weight-semibold">上次更新</th>
</tr>
</thead>
<tbody class="text-body-1">
<tr v-for="(item,index) in hotGuardStore.guards" :key="item.id">
<td class="font-weight-bold">
<div class="d-flex align-center py-1">
<div class="ml-5">
<p class="font-weight-bold">{{ item.guardName }}</p>
</div>
</div>
</td>
<td>
<div class="d-flex align-center py-1">
<div class="ml-5">
<p class="font-weight-bold">{{ nanosToHMS_CN(item.delayTime) }}</p>
</div>
</div>
</td>
<td>
<div class="d-flex align-center py-1">
<div class="ml-5">
<p class="font-weight-bold">{{ item.failRetryTimes }}</p>
</div>
</div>
</td>
<td>
<div class="d-flex align-center py-1">
<div class="ml-5">
<p class="font-weight-bold">{{isoStrToNormal(item.preGuardTime)}}</p>
</div>
</div>
</td>
</tr>
</tbody>
</v-table>
</template>
<script setup lang="ts">
import {getGuards, getHotGuardSetting} from "@/api/hot/hotGuardApi";
import {useHotGuardStore} from "@/views/app/hot/hot_guard/hotGuardStore";
import {nanosToHMS_CN,isoStrToNormal} from "@/utils/timeUtils";
const hotGuardStore = useHotGuardStore()
onMounted(async()=>{
await getGuards().then(res=>{
hotGuardStore.guards = res.data['list']
})
})
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,223 @@
<template>
<v-dialog v-model="dialog" max-width="700">
<v-card>
<v-card-title class="pa-4 bg-secondary">
<span class="title text-white">Edit Setting</span>
</v-card-title>
<v-card-text>
<v-form
class="mt-5"
ref="form"
v-model="refForm"
lazy-validation
>
<v-row>
<v-col cols="12" sm="6">
<v-text-field
variant="outlined"
color="primary"
density="compact"
:rules="timeRules"
v-model="editedItem.updateHotModuleTimes"
label="热门模块更新时间(毫秒)"
type="email"
></v-text-field>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
variant="outlined"
color="primary"
density="compact"
:rules="timeRules"
v-model="editedItem.updateHotLivesTimes"
label="热门直播更新时间(毫秒)"
type="phone"
></v-text-field>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn color="error" @click="close">Cancel</v-btn>
<v-btn
color="secondary"
:disabled="saveBtnDisable(editedItem)"
variant="flat"
@click="save"
>Save</v-btn
>
</v-card-actions>
</v-card>
</v-dialog>
<v-table class="mt-5">
<thead>
<tr>
<th class="text-subtitle-1 font-weight-semibold">Id</th>
<th class="text-subtitle-1 font-weight-semibold">直播平台</th>
<th class="text-subtitle-1 font-weight-semibold">热门模块监控</th>
<th class="text-subtitle-1 font-weight-semibold">热门直播监控</th>
<th class="text-subtitle-1 font-weight-semibold">跟风狗模式</th>
<th class="text-subtitle-1 font-weight-semibold">热门模块更新时间</th>
<th class="text-subtitle-1 font-weight-semibold">热门直播更新时间</th>
<th class="text-subtitle-1 font-weight-semibold">操作</th>
</tr>
</thead>
<tbody class="text-body-1">
<tr v-for="(item,index) in hotGuardStore.getSettings" :key="item.id">
<td class="font-weight-bold">{{ item.id }}</td>
<td>
<div class="d-flex align-center py-1">
<div class="ml-5">
<p class="font-weight-bold">{{ item.platform }}</p>
</div>
</div>
</td>
<td>
<v-switch class="flex-col"
size="small"
style="padding-right: 65%"
v-model="item.enableHotModule"
color="rgb(33,150,243)"
@click="updateSwitch(item)"
inset
></v-switch>
</td>
<td>
<v-switch class="flex-col"
style="padding-right: 65%"
size="small"
v-model="item.enableHotLive"
color="rgb(33,150,243)"
@click="updateSwitch(item)"
inset
></v-switch>
</td>
<td>
<v-switch class="flex-col"
style="padding-right: 65%"
size="small"
v-model="item.followDogEnable"
color="green"
@click="updateFollowDogSwitch(item)"
inset
></v-switch>
</td>
<td>{{nanosToHMS_CN(item.updateHotModuleTimes)}}</td>
<td>{{nanosToHMS_CN(item.updateHotLivesTimes)}}</td>
<td>
<div class="d-flex align-center">
<v-tooltip text="Edit">
<template v-slot:activator="{ props }">
<v-btn
color="blue"
icon
variant="text"
@click="editItem(item,index)"
v-bind="props"
>
<v-icon>mdi-pencil-outline</v-icon>
</v-btn>
</template>
</v-tooltip>
</div>
</td>
</tr>
</tbody>
</v-table>
</template>
<script setup lang="ts">
import {getGuards, getHotGuardSetting} from "@/api/hot/hotGuardApi";
import {openFollowDog} from "@/api/hot/heatRecommendApi";
import {useHotGuardStore} from "@/views/app/hot/hot_guard/hotGuardStore";
import {nanosToHMS_CN} from "@/utils/timeUtils";
import {Setting} from "@/views/app/hot/hot_guard/hotSettingTypes";
import {useSnackbarStore} from "@/stores/snackbarStore";
const hotGuardStore = useHotGuardStore()
const snackbarStore = useSnackbarStore();
onMounted(async()=>{
await getHotGuardSetting().then(res=>{
hotGuardStore.settings = res.data['list'].map((setting: any)=>setting as Setting)
})
})
const timeRules = [
(v)=> !!v || "时间必须存在",
(v)=> v > 0 || "时间必须大于0",
(v)=> v >= 1000 || "时间必须大于1s",
]
const dialog = ref(false);
const refForm = ref<Setting>();
const editedItem = ref<Setting>();
const editedIndex = ref(-1);
const defaultItem = ref<Setting>({
id: 0,
platform:"",
failRetryTimes:0,
enableHotModule:false,
enableHotLive:false,
followDogEnable:false,
updateHotModuleTimes:0,
updateHotLivesTimes:0
});
//Methods
function saveBtnDisable(item: any){
const setting = item as Setting
return timeRules.some((rule)=>typeof rule(setting.updateHotModuleTimes) === "string")||
timeRules.some((rule)=>typeof rule(setting.updateHotLivesTimes) === "string")
}
function editItem(item: Setting,index: number) {
editedIndex.value = index;
editedItem.value = Object.assign({}, item);
dialog.value = true;
}
function updateSwitch(item: Setting){
hotGuardStore.updateSettingNoIndex(item)
}
function updateFollowDogSwitch(item: Setting){
openFollowDog(item.platform,!item.followDogEnable).then(res=>{
if(res?.data?.success){
if(item.followDogEnable){
snackbarStore.showSuccessMessage(item.platform+"跟风狗模式已打开!")
}else{
snackbarStore.showSuccessMessage(item.platform+"跟风狗模式已关闭!")
}
}else{
if(item.followDogEnable){
snackbarStore.showSuccessMessage(item.platform+"跟风狗模式打开失败!")
}else{
snackbarStore.showSuccessMessage(item.platform+"跟风狗模式已关闭失败!")
}
item.followDogEnable = !item.followDogEnable
}
})
}
function close() {
dialog.value = false;
editedItem.value = Object.assign({}, defaultItem.value);
editedIndex.value = -1;
}
function save() {
const setting = Object.assign({}, editedItem.value)
hotGuardStore.updateSetting(editedIndex.value,setting)
close();
}
</script>
<style scoped>
</style>

Binary file not shown.