修改并完善淡白影视代码

修改并完善淡白影视代码

Scroll Down

学习了spring框架之后为了巩固下基础就写了淡白影视这样一个项目

Github直达

用阿里代码检测插件检测代码规范并修改

TIM截图20191013192806.png
使用未定义常量"user"
TIM截图20191013192930.png
给接口、方法、类添加注释
TIM截图20191013202219.png

实现访问统计

使用redis存储数据所以先配置所需依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Application

redis:
    database: 0
    host: 39.108.110.44
    password: ****
    port: 6379
    timeout: 0

配置redisConfig

package com.danbai.ys.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
 * @author danbai
 * @date 2019-10-14 14:36
 */
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
        //如使用注解的话需要配置cacheManager
    CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        //初始化一个RedisCacheWriter
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig();
        //设置默认超过期时间是1天
        defaultCacheConfig.entryTtl(Duration.ofDays(1));
        //初始化RedisCacheManager
        RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, defaultCacheConfig);
        return cacheManager;
    }

    // 以下两种redisTemplate自由根据场景选择
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);

        template.setValueSerializer(serializer);
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }

    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        stringRedisTemplate.setConnectionFactory(factory);
        return stringRedisTemplate;
    }
}

测试类

@RunWith(SpringRunner.class)
@SpringBootTest(classes = YsApplication.class)
public class YsApplicationTests {
    @Autowired
    RedisTemplate redisTemplate;
    @Test
    public void redis() {
        //这里相当于redis对String类型的set操作
        redisTemplate.opsForValue().set("test","哈喽!");
        //这里相当于redis对String类型的get操作
        String test = (String)redisTemplate.opsForValue().get("test");
        System.out.println(test);
    }

}

然后在redis新建一个key为TotalAccess值为1的数据

redisTemplate.opsForValue().increment("TotalAccess");

就能增加访问统计了
reids相关使用看了下这篇文章
接下来在Controller添加

 @ModelAttribute
    void count(HttpServletRequest request) {
        String ip = IpUtils.getIpAddr(request);
        if (!statistical.isIpInTheDatabase(ip)) {
            statistical.addIp(ip);
            statistical.addAccess();
        }
    }

统计服务实现

@Service
public class StatisticalImpl implements Statistical {
    @Autowired
    RedisTemplate redisTemplate;

    @Override
    public void addAccess() {
        redisTemplate.opsForValue().increment("TotalAccess");
    }

    @Override
    public boolean isIpInTheDatabase(String ip) {
        return redisTemplate.opsForSet().isMember(IpUtils.getDay(), ip);
    }
    @Override
    public void addIp(String ip) {
        redisTemplate.opsForSet().add(IpUtils.getDay(), ip);
        redisTemplate.opsForValue().increment(IpUtils.getDay() + "-Access");
        redisTemplate.expire(IpUtils.getDay(),1,TimeUnit.DAYS);
        redisTemplate.expire(IpUtils.getDay() + "-Access",1,TimeUnit.DAYS);
    }

    @Override
    public int getAccess() {
        return (int) redisTemplate.opsForValue().get("TotalAccess");
    }

    @Override
    public int getDayAccess() {
        return (int) redisTemplate.opsForValue().get(IpUtils.getDay() + "-Access");
    }
}

增加弹幕获取功能

从腾讯视频获取弹幕
实现方式
首先当用户请求影视页面时候通过请求id获取影视信息,然后再在腾讯视频搜索视频.获取腾讯视频的影视id再通过

http://s.video.qq.com/get_playsource?plat=2&type=4&range=1&otype=json&id=" + id

获取影视的视频列表
TIM截图20191015181854.png
解析json获取其中视频id再通过接口
http://bullet.video.qq.com/fcgi-bin/target/regist?otype=json&vid=影视id
获取弹幕id (targetid)

QZOutputJson={"danmukey":"bubble_flag=1&targetid=4193003793&vid=c0032vrylb9&type=2","display":1,"is_has_adv":0,"is_has_bubble":1,"open":1,"returncode":0,"returnmsg":"OK","targetid":"4193003793"};

最后通过接口 targetid+时间获取弹幕列表添加到视频播放器
https://mfm.video.qq.com/danmu?otype=json&target_id=4193003793&timestamp=360

视频播放器Dplayer

最后实现结果
TIM截图20191015182425.png
这次功能中遇到的困难不少,如跨域问题ip代理问题还有播放器如何添加弹幕等等.
详情阅读源码

实现弹幕保存到数据库

在向服务器请求弹幕id的时候判断服务器是否保存过该影视的弹幕.
20191017145646.png
存在: 返回null
不存在: 将请求信息存入redis

实现异步任务线程池

创建异步线程池配置类

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(20);
        executor.setMaxPoolSize(100);
        executor.setQueueCapacity(100);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("taskExecutor-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

给Application添加异步和任务调度开启注解

@EnableScheduling
@EnableAsync

实现调度任务

创建Scheduler 任务调度类

@Component
@Configurable
public class Scheduler {
    @Autowired
    RedisTemplate redisTemplate;
    @Autowired
    private Dmas testas;
    @Scheduled(fixedDelay = 10000)
    @Async
    public void cronJobSchedule() {
        Set tagids = redisTemplate.opsForSet().members("tagids");
        redisTemplate.delete("tagids");
        Object[] das = tagids.toArray();
        System.out.println(das.length);
        for (Object s : das) {
            JSONObject jsonObject = JSON.parseObject(String.valueOf(s));
            String tagid = jsonObject.getString("tagid");
            redisTemplate.opsForSet().add("oktagids", tagid);
            String player = (jsonObject.getString("player"));
            redisTemplate.delete("danmaku"+player);
            int timestamp = 0;
            boolean flg=true;
            while (flg) {
                String url = "http://mfm.video.qq.com/danmu?otype=json&target_id=" + tagid + "&timestamp=" + timestamp;
                timestamp += 30;
                testas.xzbcdm(url,player);
                if(timestamp>60*120){
                    flg=false;
                }
            }
        }
    }
}

异步任务类

package com.danbai.ys.async;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.danbai.ys.entity.Dan;
import com.danbai.ys.utils.HtmlUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;


import static java.lang.System.currentTimeMillis;

/**
 * @author danbai
 * @date 2019-10-17 09:12
 */
@Component
public class Dmas {
    @Autowired
    MongoTemplate mongoTemplate;
    @Async
    public void xzbcdm(String url, String player){
        String json = HtmlUtils.getHtmlContentNp(url);
        JSONObject jsonObject = JSON.parseObject(json);
        JSONArray comments = jsonObject.getJSONArray("comments");
        int maxc = comments.size();
        for (int j = 0; j < maxc; j++) {
            JSONObject comment = comments.getJSONObject(j);
            Dan d = new Dan();
            d.setReferer("https://v.qq.com");
            d.setIp("::ffff:111.111.111.111");
            d.setType(0);
            d.setTime(comment.getDouble("timepoint"));
            d.setAuthor(comment.getString("opername"));
            d.setPlayer(player);
            d.setText(comment.getString("content"));
            d.setColor(14277107);
            d.setDate(currentTimeMillis());
            mongoTemplate.insert(d);
        }
    }
}

大概意思就是上次任务结束10秒钟后(启动应用执行一次,每次执行在上一次执行10后)执行一次.
检查redis任务队列是否有任务
有就执行任务并将弹幕数据存入mongodb
执行的任务开启@Async 表示异步执行
值得注意的是异步任务不能调用当前类中方法异步执行.
获取到的弹幕数据
TIM截图20191017152027.png

时区问题

在前端请求后台观看历史记录时发现时间数据相差8小时
20191030152111.png
数据库
20191030154017.png

原因

spring中对于@RestController或者@Controller+@ResponseBody
注解的接口方法的返回值默认是Json格式,

所以当对于date类型的数据,在返回浏览器端是会被spring-boot
默认的Jackson框架转换,而Jackson框架默认的时区GMT(相对于中国是少了8小时)。

解决方法

在配置下多增加一个配置 第二个用于转换格式(可不用)

  jackson:
    time: GMT+8
    date-format: yyyy-MM-dd HH:mm:ss