前言
继上一篇使用Flutter开发的抖音国际版后再次撸一个国内版抖音,大部分功能已完成,主要是Flutter开发APP速度很爽, 先看下图
项目主要结构介绍
这次主要的改动在api.dart 及douyin.dart里,国内抖音的api是不同的,另外地址以及实体类也不一样。详细下面介绍.
抖音的实体类
主要是讲json转化为实体模型,然后绑定到view 层面,写这个实体类还是很耗费时间的,毕竟得先用爬网知识讲抖音的json拿到,并且根据json反向写实体类.
这个地址获取到抖音的推荐列表json: https://creator.douyin.com/aweme/v1/creator/data/billboard/?billboard_type=4,绑定第一个Douyin实体,拿到所有抖音的url地址
通过第二个api地址 https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids= 拿到抖音视频所有的json信息并且绑定VideoData实体类
class Douyin {
int statusCode;
String statusMsg;
List
Extra extra;
Douyin({this.statusCode, this.statusMsg, this.billboardData, this.extra});
Douyin.fromJson(Map
statusCode = json["status_code"];
statusMsg = json["status_msg"];
if (json['billboard_data'] != null) {
billboardData = new List
json["billboard_data"].forEach((v) {
billboardData.add(BillboardData.fromJson(v));
});
}
extra = json['extra'] != null ? new Extra.fromJson(json['extra']) : null;
}
Map
final Map
data["status_code"] = this.statusCode;
data["status_msg"] = this.statusMsg;
if (this.billboardData != null) {
data["billboard_data"] =
this.billboardData.map((v) => v.toJson()).toList();
}
data["extra"] = this.extra;
return data;
}
}
class BillboardData {
String author;
String imgUrl;
String link;
int rank;
String title;
String value;
BillboardData(
{this.author, this.imgUrl, this.link, this.rank, this.title, this.value});
BillboardData.fromJson(Map
this.author = json["author"];
this.imgUrl = json["img_url"];
this.link = json["link"];
this.rank = json["rank"];
this.title = json["title"];
this.value = json["value"];
}
Map
final Map
data["author"] = this.author;
data["img_url"] = this.imgUrl;
data["link"] = this.link;
data["rank"] = this.rank;
data["title"] = this.title;
data["value"] = this.value;
return data;
}
}
class Extra {
int now;
Extra({this.now});
Extra.fromJson(Map
now = json["now"];
}
Map
final Map
data["now"] = this.now;
return data;
}
}
class VideoData {
int statusCode;
List
ExtraData extra;
VideoData.fromJson(Map
this.statusCode = json["status_code"];
if (json['item_list'] != null) {
itemList = new List
json['item_list'].forEach((v) {
itemList.add(new Itemlist.fromJson(v));
});
}
this.extra =
json["extra"] != null ? new ExtraData.fromJson(json["extra"]) : null;
}
Map
final Map
data["status_code"] = this.statusCode;
if (this.itemList != null) {
data['item_list'] = this.itemList.map((v) => v.toJson()).toList();
}
if (this.extra != null) {
data["extra"] = this.extra.toJson();
}
return data;
}
}
class Itemlist {
String awemeid;
String videolabels;
String labeltoptext;
int category;
Author author;
int duration;
String promotions;
int ispreview;
int createtime;
List
String commentlist;
int authoruserid;
String videotext;
int groupid;
bool islivereplay;
ShareInfo shareinfo;
String position;
String imageinfos;
RiskInfos riskinfos;
String uniqidposition;
String geofencing;
Statistics statistics;
int awemetype;
Music music;
List
Video video;
String shareurl;
String desc;
String longvideo;
Itemlist(
{this.awemeid,
this.videolabels,
this.labeltoptext,
this.category,
this.author,
this.duration,
this.promotions,
this.ispreview,
this.createtime,
this.chalist,
this.commentlist,
this.authoruserid,
this.videotext,
this.groupid,
this.islivereplay,
this.shareinfo,
this.position,
this.imageinfos,
this.riskinfos,
this.uniqidposition,
this.geofencing,
this.statistics,
this.awemetype,
this.music,
this.textExtras,
this.video,
this.shareurl,
this.desc,
this.longvideo});
Itemlist.fromJson(Map
this.awemeid = json["aweme_id"];
this.videolabels = json["video_labels"];
this.labeltoptext = json["label_top_text"];
this.category = json["category"];
this.author =
json["author"] != null ? new Author.fromJson(json["author"]) : null;
this.duration = json["duration"];
this.promotions = json["promotions"];
this.ispreview = json["is_preview"];
this.createtime = json["create_time"];
if (json["cha_list"] != null) {
this.chalist = new List
json["cha_list"].forEach((v) {
this.chalist.add(new ChaList.fromJson(v));
});
}
this.commentlist = json["comment_list"];
this.authoruserid = json["author_user_id"];
this.videotext = json["video_text"];
this.groupid = json["group_id"];
this.islivereplay = json["is_live_replay"];
this.shareinfo = json["share_info"] != null
? new ShareInfo.fromJson(json["share_info"])
: null;
this.position = json["position"];
this.imageinfos = json["image_infos"];
this.riskinfos = json["risk_infos"] != null
? new RiskInfos.fromJson(json["risk_infos"])
: null;
this.uniqidposition = json["uniqid_position"];
this.geofencing = json["geofencing"];
this.statistics = json["statistics"] != null
? new Statistics.fromJson(json["statistics"])
: null;
this.awemetype = json["aweme_type"];
this.music =
json["music"] != null ? new Music.fromJson(json["music"]) : null;
if (json["text_extra"] != null) {
this.textExtras = new List
json['text_extra'].forEach((v) {
textExtras.add(new TextExtra.formJson(v));
});
}
this.video =
json["video"] != null ? new Video.fromJson(json["video"]) : null;
this.shareurl = json["share_url"];
this.desc = json["desc"];
this.longvideo = json["long_video"];
}
Map
final Map
data["aweme_id"] = this.awemeid;
data["video_labels"] = this.videolabels;
data["label_top_text"] = this.labeltoptext;
data["category"] = this.category;
if (this.author != null) {
data["author"] = this.author.toJson();
}
data["duration"] = this.duration;
data["promotions"] = this.promotions;
data["is_preview"] = this.ispreview;
data["create_time"] = this.createtime;
if (this.chalist != null) {
data["cha_list"] = this.chalist.map((e) => e.toJson()).toList();
}
data["comment_list"] = this.commentlist;
data["author_user_id"] = this.authoruserid;
data["video_text"] = this.videotext;
data["group_id"] = this.groupid;
data["is_live_replay"] = this.islivereplay;
if (this.shareinfo != null) {
data["share_info"] = this.shareinfo.toJson();
}
data["position"] = this.position;
data["image_infos"] = this.imageinfos;
if (this.riskinfos != null) {
data["risk_infos"] = this.riskinfos.toJson();
}
data["uniqid_position"] = this.uniqidposition;
data["geofencing"] = this.geofencing;
if (this.statistics != null) {
data["statistics"] = this.statistics.toJson();
}
data["aweme_type"] = this.awemetype;
if (this.music != null) {
data["music"] = this.music.toJson();
}
if (this.textExtras != null) {
data["text_extra"] = this.textExtras.map((e) => e.toJson()).toList();
}
if (this.video != null) {
data["video"] = this.video.toJson();
}
data["share_url"] = this.shareurl;
data["desc"] = this.desc;
data["long_video"] = this.longvideo;
return data;
}
}
class TextExtra {
String hashtagName;
int hashtagId;
int start;
int end;
int type;
TextExtra(
{this.hashtagName, this.hashtagId, this.start, this.end, this.type});
TextExtra.formJson(Map
this.hashtagName = json["hashtag_name"];
this.hashtagId = json["hashtag_id"];
this.start = json["start"];
this.end = json["end"];
this.type = json["type"];
}
Map
final Map
data["hashtag_name"] = this.hashtagName;
data["hashtag_id"] = this.hashtagId;
data["start"] = this.start;
data["end"] = this.end;
data["type"] = this.type;
return data;
}
}
// Author
class Author {
String geofencing;
String uid;
String shortID;
String signature;
AvatarMedium avatarMedium;
String uniqueId;
String followersDetail;
String platformSyncInfo;
String policyVersion;
String nickname;
AvatarLarger avatarlarger;
AvatarThumb avatarthumb;
Author(
{this.geofencing,
this.uid,
this.shortID,
this.signature,
this.avatarMedium,
this.uniqueId,
this.followersDetail,
this.platformSyncInfo,
this.policyVersion,
this.nickname,
this.avatarlarger,
this.avatarthumb});
Author.fromJson(Map
this.geofencing = json["geofencing"];
this.uid = json["uid"];
this.shortID = json["short_id"];
this.signature = json["signature"];
this.avatarMedium = json["avatar_medium"] != null
? new AvatarMedium.fromJson(json["avatar_medium"])
: null;
this.uniqueId = json["unique_id"];
this.followersDetail = json["followers_detail"];
this.platformSyncInfo = json["platform_sync_info"];
this.policyVersion = json["policy_version"];
this.nickname = json["nickname"];
this.avatarlarger = json["avatar_larger"] != null
? new AvatarLarger.formJson(json["avatar_larger"])
: null;
this.avatarthumb = json["avatar_thumb"] != null
? new AvatarThumb.formJson(json["avatar_thumb"])
: null;
}
Map
final Map
data["geofencing"] = this.geofencing;
data["uid"] = this.uid;
data["short_id"] = this.shortID;
data["signature"] = this.signature;
if (this.avatarMedium != null) {
data["avatar_medium"] = this.avatarMedium.toJson();
}
data["unique_id"] = this.uniqueId;
data["followers_detail"] = this.followersDetail;
data["platform_sync_info"] = this.platformSyncInfo;
data["policy_version"] = this.policyVersion;
data["nickname"] = this.nickname;
if (this.avatarlarger != null) {
data["avatar_larger"] = this.avatarlarger.toJson();
}
if (this.avatarthumb != null) {
data["avatar_thumb"] = this.avatarthumb.toJson();
}
return data;
}
}
class AvatarMedium {
List
String uri;
AvatarMedium({this.urlList, this.uri});
AvatarMedium.fromJson(Map
if (json["url_list"] != null) {
this.urlList = json["url_list"].cast
}
this.uri = json["uri"];
}
Map
final Map
data["url_list"] = this.urlList;
return data;
}
}
class AvatarLarger {
String uri;
List
AvatarLarger({this.uri, this.urlList});
AvatarLarger.formJson(Map
this.uri = json["uri"] as String;
this.urlList = json["url_list"].cast
}
Map
final Map
data["url_list"] = this.urlList;
data["uri"] = this.uri;
return data;
}
}
class AvatarThumb {
String uri;
List
AvatarThumb({this.uri, this.urlList});
AvatarThumb.formJson(Map
this.uri = json["uri"] as String;
this.urlList = json["url_list"].cast
}
Map
final Map
data["url_list"] = this.urlList;
data["uri"] = this.uri;
return data;
}
}
class ExtraData {
int now;
String logid;
ExtraData({this.now, this.logid});
ExtraData.fromJson(Map
this.now = json["now"];
this.logid = json["logid"];
}
Map
final Map
data["now"] = this.now;
data["logid"] = this.logid;
return data;
}
}
//End author
//ChaList
class ChaList {
String chaName;
int viewCount;
String hashTagProfile;
bool isCommerce;
String cid;
String desc;
int userCount;
String connectMusic;
int type;
CoverItem coverItem;
ChaList(
{this.chaName,
this.viewCount,
this.hashTagProfile,
this.isCommerce,
this.cid,
this.desc,
this.userCount,
this.connectMusic,
this.type,
this.coverItem});
ChaList.fromJson(Map
this.chaName = json["cha_name"];
this.viewCount = json["view_count"];
this.hashTagProfile = json["hash_tag_profile"];
this.isCommerce = json["is_commerce"];
this.cid = json["cid"];
this.desc = json["desc"];
this.userCount = json["user_count"];
this.connectMusic = json["connect_music"];
this.type = json["type"];
this.coverItem = json["cover_item"] != null
? new CoverItem.formJson(json["cover_item"])
: null;
}
Map
final Map
data["cha_name"] = this.chaName;
data["view_count"] = this.viewCount;
data["hash_tag_profile"] = this.hashTagProfile;
data["is_commerce"] = this.isCommerce;
data["cid"] = this.cid;
data["desc"] = this.desc;
data["user_count"] = this.userCount;
data["connect_music"] = this.connectMusic;
data["type"] = this.type;
if (this.coverItem != null) {
data["cover_item"] = this.coverItem.toJson();
}
return data;
}
}
class CoverItem {
String uri;
List
CoverItem({this.uri, this.urlList});
CoverItem.formJson(Map
this.uri = json["uri"] as String;
this.urlList = json["url_list"].cast
}
Map
final Map
data["url_list"] = this.urlList;
data["uri"] = this.uri;
return data;
}
}
//End ChaList
//ShareInfo
class ShareInfo {
String shareweibodesc;
String sharedesc;
String sharetitle;
ShareInfo({this.shareweibodesc, this.sharedesc, this.sharetitle});
ShareInfo.fromJson(Map
this.shareweibodesc = json["share_weibo_desc"];
this.sharedesc = json["share_desc"];
this.sharetitle = json["share_title"];
}
Map
final Map
data["share_weibo_desc"] = this.shareweibodesc;
data["share_desc"] = this.sharedesc;
data["share_title"] = this.sharetitle;
return data;
}
}
//End ShareInfo
//RiskInfos
class RiskInfos {
bool warn;
int type;
String content;
RiskInfos({this.warn, this.type, this.content});
RiskInfos.fromJson(Map
this.warn = json["warn"] as bool;
this.type = json["type"];
this.content = json["content"];
}
Map
final Map
data["warn"] = this.warn;
data["type"] = this.type;
data["content"] = this.content;
return data;
}
}
//End RiskInfos
//Statistics
class Statistics {
String awemeId;
int commentCount;
int diggCount;
Statistics.fromJson(Map
this.awemeId = json["aweme_id"];
this.commentCount = json["comment_count"];
this.diggCount = json["digg_count"];
}
Map
final Map
data["aweme_id"] = this.awemeId;
data["comment_count"] = this.commentCount;
data["digg_count"] = this.diggCount;
return data;
}
}
//End Statistics
//Music
class Music {
CoverLarge coverlarge;
CoverMedium covermedium;
int duration;
int status;
String mid;
String title;
String author;
PlayUrl playurl;
String position;
int id;
CoverHd coverhd;
CoverThumb coverthumb;
Music.fromJson(Map
this.coverlarge = json["cover_large"] != null
? new CoverLarge.formJson(json["cover_large"])
: null;
this.covermedium = json["cover_medium"] != null
? new CoverMedium.formJson(json["cover_medium"])
: null;
this.duration = json["duration"];
this.status = json["status"];
this.mid = json["mid"];
this.title = json["title"];
this.author = json["author"];
this.playurl = json["play_url"] != null
? new PlayUrl.formJson(json["play_url"])
: null;
this.position = json["position"];
this.id = json["id"];
this.coverhd = json["cover_hd"] != null
? new CoverHd.formJson(json["cover_hd"])
: null;
this.coverthumb = json["cover_thumb"] != null
? new CoverThumb.formJson(json["cover_thumb"])
: null;
}
Map
final Map
if (this.coverlarge != null) {
data["cover_large"] = this.coverlarge.toJson();
}
if (this.covermedium != null) {
data["cover_medium"] = this.covermedium.toJson();
}
data["duration"] = this.duration;
data["status"] = this.status;
data["mid"] = this.mid;
data["title"] = this.title;
data["author"] = this.author;
if (this.playurl != null) {
data["play_url"] = this.playurl.toJson();
}
data["position"] = this.position;
data["id"] = this.id;
if (this.coverhd != null) {
data["cover_hd"] = this.coverhd.toJson();
}
if (this.coverthumb != null) {
data["cover_thumb"] = this.coverthumb.toJson();
}
return data;
}
}
class CoverLarge {
String uri;
List
CoverLarge({this.uri, this.urlList});
CoverLarge.formJson(Map
this.uri = json["uri"] as String;
this.urlList = json["url_list"].cast
}
Map
final Map
data["url_list"] = this.urlList;
data["uri"] = this.uri;
return data;
}
}
class CoverMedium {
String uri;
List
CoverMedium({this.uri, this.urlList});
CoverMedium.formJson(Map
this.uri = json["uri"] as String;
this.urlList = json["url_list"].cast
}
Map
final Map
data["url_list"] = this.urlList;
data["uri"] = this.uri;
return data;
}
}
class PlayUrl {
String uri;
List
PlayUrl({this.uri, this.urlList});
PlayUrl.formJson(Map
this.uri = json["uri"] as String;
this.urlList = json["url_list"].cast
}
Map
final Map
data["url_list"] = this.urlList;
data["uri"] = this.uri;
return data;
}
}
class CoverHd {
String uri;
List
CoverHd({this.uri, this.urlList});
CoverHd.formJson(Map
this.uri = json["uri"] as String;
this.urlList = json["url_list"].cast
}
Map
final Map
data["url_list"] = this.urlList;
data["uri"] = this.uri;
return data;
}
}
class CoverThumb {
String uri;
List
CoverThumb({this.uri, this.urlList});
CoverThumb.formJson(Map
this.uri = json["uri"] as String;
this.urlList = json["url_list"].cast
}
Map
final Map
data["url_list"] = this.urlList;
data["uri"] = this.uri;
return data;
}
}
//End Music
//Video
class Video {
int width;
OriginCover origincover;
String ratio;
bool haswatermark;
String bitrate;
PlayAddr playaddr;
Cover cover;
int duration;
String vid;
int islongvideo;
int height;
DynamicCover dynamiccover;
Video(
{this.width,
this.origincover,
this.ratio,
this.haswatermark,
this.bitrate,
this.playaddr,
this.cover,
this.duration,
this.vid,
this.islongvideo,
this.height,
this.dynamiccover});
Video.fromJson(Map
this.width = json["width"];
this.origincover = json["origin_cover"] != null
? new OriginCover.formJson(json["origin_cover"])
: null;
this.ratio = json["ratio"];
this.haswatermark = json["has_watermark"];
this.bitrate = json["bit_rate"];
this.playaddr = json["play_addr"] != null
? new PlayAddr.formJson(json["play_addr"])
: null;
this.cover =
json["cover"] != null ? new Cover.formJson(json["cover"]) : null;
this.duration = json["duration"];
this.vid = json["vid"];
this.islongvideo = json["is_long_video"];
this.height = json["height"];
this.dynamiccover = json["dynamic_cover"] != null
? new DynamicCover.formJson(json["dynamic_cover"])
: null;
}
Map
final Map
data["width"] = this.width;
if (this.origincover != null) {
data["origin_cover"] = this.origincover.toJson();
}
data["ratio"] = this.ratio;
data["has_watermark"] = this.haswatermark;
data["bit_rate"] = this.bitrate;
if (this.playaddr != null) {
data["play_addr"] = this.playaddr.toJson();
}
if (this.cover != null) {
data["cover"] = this.cover.toJson();
}
data["duration"] = this.duration;
data["vid"] = this.vid;
data["is_long_video"] = this.islongvideo;
data["height"] = this.height;
if (this.dynamiccover != null) {
data["dynamic_cover"] = this.dynamiccover.toJson();
}
return data;
}
}
class OriginCover {
String uri;
List
OriginCover({this.uri, this.urlList});
OriginCover.formJson(Map
this.uri = json["uri"] as String;
this.urlList = json["url_list"].cast
}
Map
final Map
data["url_list"] = this.urlList;
data["uri"] = this.uri;
return data;
}
}
class PlayAddr {
String uri;
List
PlayAddr({this.uri, this.urlList});
PlayAddr.formJson(Map
this.uri = json["uri"] as String;
this.urlList = json["url_list"].cast
}
Map
final Map
data["url_list"] = this.urlList;
data["uri"] = this.uri;
return data;
}
}
class Cover {
String uri;
List
Cover({this.uri, this.urlList});
Cover.formJson(Map
this.uri = json["uri"] as String;
this.urlList = json["url_list"].cast
}
Map
final Map
data["url_list"] = this.urlList;
data["uri"] = this.uri;
return data;
}
}
class DynamicCover {
String uri;
List
DynamicCover({this.uri, this.urlList});
DynamicCover.formJson(Map
this.uri = json["uri"] as String;
this.urlList = json["url_list"].cast
}
Map
final Map
data["url_list"] = this.urlList;
data["uri"] = this.uri;
return data;
}
}
//End Video
取抖音无水印视频,这里才是关键
通过这个api地址 https://aweme.snssdk.com/aweme/v1/play/?video_id={}&ratio=720p&line=0&media_type=4&vr_type=0&improve_bitrate=0&is_play_url=1&h265=1&adapt720=1 可以成功的拿到无水印视频,
此方法仅用于学习研究目的,不得从事违法活动哈, 否则与本作者无关.
具体实现代码如下
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart' as http;
import 'package:dio/dio.dart';
class RequestController {
//static String host = "https://www.tiktok.com/";
static String host = "https://creator.douyin.com";
String url = host + "/aweme/v1/creator/data/billboard/?billboard_type=4";
String video =
"https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=";
String player = "https://aweme.snssdk.com/aweme/v1/play/?video_id=";
Future
try {
var response = await http.get(url);
return response.body;
} catch (e) {
return e.toString();
}
}
//获取无水印的视频
Future
try {
var response = await new Dio().get(
player +
videoid +
"&ratio=720p&line=0&media_type=4&vr_type=0&improve_bitrate=0&is_play_url=1&h265=1&adapt720=1",
options: Options(
headers: headers,
contentType: "text/html; charset=utf-8",
followRedirects: false,
validateStatus: (status) {
return status < 500;
}),
);
if (response.statusCode == 302) {
return response.data.toString().split('"')[1];
}
return '';
} catch (ex) {
return '';
}
}
Future
try {
var response = await http.get(host + "/share/item/");
return response.headers["set-cookie"];
} catch (e) {
return "error";
}
}
var headers = {
"user-agent":
"Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1"
};
}
剩下就是改造TrendingScreen里获取视频的方法
getTrending() async {
//var cookies = await api.getCookie();
//api.setCookie(cookies);
try {
var response = await http.get(
api.url,
headers: api.headers,
);
Douyin tiktok = Douyin.fromJson(jsonDecode(response.body));
tiktok.billboardData.forEach(
(item) {
setState(() {
getVideos(item);
});
},
);
} catch (ex) {
SimpleDialog(
title: Text('Hot videos list is empty'),
);
print(ex);
}
}
把获取到的无水印视频加载到view层
getVideos(BillboardData v) async {
try {
var url = v.link.split("/")[5];
var response = await http.get(
api.video + url + "&dytk",
headers: api.headers,
);
VideoData videoData = VideoData.fromJson(jsonDecode(response.body));
//获取无水印的视频地址
api.getRedirects(videoData.itemList[0].video.playaddr.uri).then((url) => {
print( Uri.decodeFull(url)),
if (url != '')
{
videos.add(VideoItem(
data: videoData,
videourl: url,
))
}
});
} catch (ex) {
print(ex);
}
}
最后,就是绑定视频数据啦,大功告成!
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children:
DouyinVideoPlayer(
url: videourl,
),
title(),
VideoDescription(
description: data.itemList[0].textExtras[0].hashtagName,
musicName: data.itemList[0].music.title,
authorName: data.itemList[0].music.author,
userName: data.itemList[0].author.nickname,
),
ActionsToolbar(
comments: data.itemList[0].statistics.commentCount.toString(),
userImg: data.itemList[0].author.avatarMedium.urlList[0],
favorite: data.itemList[0].statistics.diggCount,
coverImg: data.itemList[0].music.covermedium.urlList[0],
),
],
),
);
}
结语
写到这里,手撸一个抖音app完成了基本功能,一些数据转化比喻点赞数还是原始int数据,需要转化成K M 等,后续待完善,毕竟花了一天时间来写这个app纯粹为了兴趣. 本人也极喜欢玩抖音.
另外因为写得匆忙,只测试了少量数据,json返回的数据有些为null ,也不知道具体是啥类型,这个待完善。 各位博友们觉得感兴趣的点个赞哈. 支持支持!
顺便说一下,各位支持的话点赞关注我继续完善剩余的部分,有需要学习了解的可以联系我,代码上传到github ,地址:https://github.com/WangCharlie/douyin,到这里就点个star, 谢谢.