339 lines
9.5 KiB
Dart
339 lines
9.5 KiB
Dart
// ignore_for_file: prefer_const_literals_to_create_immutables, avoid_print
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:fip_parser_ui/queries.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:graphql/client.dart';
|
|
import 'package:retry/retry.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:kplayer/kplayer.dart';
|
|
|
|
const int queryTimeout = 8;
|
|
late String token;
|
|
const hours = 3;
|
|
const String radio = 'groove';
|
|
PlayerController? player;
|
|
|
|
void main() {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
HttpOverrides.global = MyHttpOverrides();
|
|
Player.boot();
|
|
runApp(const FipyApp());
|
|
}
|
|
|
|
class FipyApp extends StatelessWidget {
|
|
const FipyApp({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!File('api-token.json').existsSync()) {
|
|
print(
|
|
'Please copy "api-token.json.template" to "api-token.json" and set your custom API token.\nMore information here: https://developers.radiofrance.fr/projects/new');
|
|
exit(1);
|
|
}
|
|
final tokenFile = File('api-token.json').readAsStringSync();
|
|
final jsonData = json.decode(tokenFile);
|
|
token = jsonData['token'];
|
|
|
|
// final radioList = [
|
|
// 'fip',
|
|
// 'electro',
|
|
// 'groove',
|
|
// 'rock',
|
|
// 'jazz',
|
|
// 'pop',
|
|
// 'reggae',
|
|
// 'world',
|
|
// 'nouveautes',
|
|
// ];
|
|
|
|
// #######
|
|
|
|
return MaterialApp(
|
|
title: 'Fipy',
|
|
theme: ThemeData(
|
|
primarySwatch: Colors.blue, backgroundColor: Colors.grey[900]),
|
|
home: const HomePage(title: 'Fip moi ça'),
|
|
);
|
|
}
|
|
}
|
|
|
|
class HomePage extends StatefulWidget {
|
|
const HomePage({Key? key, required this.title}) : super(key: key);
|
|
|
|
final String title;
|
|
|
|
@override
|
|
State<HomePage> createState() => _HomePageState();
|
|
}
|
|
|
|
class _HomePageState extends State<HomePage> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
body: SingleChildScrollView(
|
|
child: Column(
|
|
children: <Widget>[
|
|
Container(
|
|
color: Colors.grey[900],
|
|
height: 50,
|
|
child: Center(
|
|
child: Text(
|
|
'Radio: ' + radio + '\nHistorique: ' + hours.toString() + 'h',
|
|
style: globalTextStyle,
|
|
),
|
|
),
|
|
),
|
|
FutureBuilder<List<Track>>(
|
|
future: getTracks(),
|
|
builder: (
|
|
BuildContext context,
|
|
AsyncSnapshot<List<Track>> snapshot,
|
|
) {
|
|
print(snapshot.connectionState);
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return const CircularProgressIndicator();
|
|
} else if (snapshot.connectionState == ConnectionState.done) {
|
|
if (snapshot.hasError) {
|
|
return const Text('Error');
|
|
} else if (snapshot.hasData) {
|
|
return Table(
|
|
defaultVerticalAlignment:
|
|
TableCellVerticalAlignment.middle,
|
|
children: snapshot.data!
|
|
.map((item) => _buildTableRow(item))
|
|
.toList()
|
|
..insert(
|
|
0,
|
|
_buildTableRow(Track(
|
|
number: -1,
|
|
title: 'Titre',
|
|
artiste: 'Artiste',
|
|
album: 'Album',
|
|
id: 'URL')),
|
|
),
|
|
);
|
|
} else {
|
|
return const Text('Empty data');
|
|
}
|
|
} else {
|
|
return Text('State: ${snapshot.connectionState}');
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Widget trackLine(Track track) {
|
|
return SizedBox(
|
|
height: 30,
|
|
child: Row(
|
|
children: [Text(track.title + ' - ' + track.artiste)],
|
|
),
|
|
);
|
|
}
|
|
|
|
class Track {
|
|
final int number;
|
|
final String title;
|
|
final String artiste;
|
|
final String? album;
|
|
String? id;
|
|
|
|
Track(
|
|
{required this.number,
|
|
required this.title,
|
|
required this.artiste,
|
|
this.album,
|
|
this.id});
|
|
}
|
|
|
|
GraphQLClient initClient() {
|
|
final _httpLink = HttpLink(
|
|
'https://openapi.radiofrance.fr/v1/graphql?x-token=$token',
|
|
);
|
|
|
|
final GraphQLClient client = GraphQLClient(
|
|
cache: GraphQLCache(),
|
|
link: _httpLink,
|
|
);
|
|
|
|
return client;
|
|
}
|
|
|
|
Future<List<Track>> getTracks() async {
|
|
final client = initClient();
|
|
|
|
final now = (DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
|
final yesterday = now - (hours * 3600);
|
|
final String radioQuery;
|
|
if (radio == 'fip') {
|
|
radioQuery = 'FIP';
|
|
} else {
|
|
radioQuery = 'FIP_${radio.toUpperCase()}';
|
|
}
|
|
|
|
final QueryOptions options = QueryOptions(
|
|
document: gql(getSongs),
|
|
pollInterval: const Duration(milliseconds: 50),
|
|
variables: <String, dynamic>{
|
|
'start': yesterday,
|
|
'end': now,
|
|
'radio': radioQuery,
|
|
},
|
|
);
|
|
|
|
QueryResult result;
|
|
result = await retry(
|
|
() async => await client
|
|
.query(options)
|
|
.timeout(const Duration(seconds: queryTimeout)),
|
|
onRetry: (_) => print('Timeout, retry...'),
|
|
);
|
|
|
|
if (result.hasException) {
|
|
print(result.exception.toString());
|
|
exit(1);
|
|
}
|
|
|
|
final List data = result.data?['grid'] ?? [];
|
|
print(data.length.toString() + ' songs');
|
|
|
|
// #######
|
|
|
|
List<Track> songsList = [];
|
|
int trackNbr = 0;
|
|
|
|
for (Map track in data) {
|
|
track = track['track'];
|
|
trackNbr++;
|
|
final String title = track['title'];
|
|
final String artiste;
|
|
if (track['mainArtists'].isNotEmpty) {
|
|
artiste = track['mainArtists'].first;
|
|
} else {
|
|
artiste = '';
|
|
}
|
|
final String album = track['albumTitle'];
|
|
|
|
final thisSong =
|
|
Track(number: trackNbr, title: title, artiste: artiste, album: album);
|
|
songsList.add(thisSong);
|
|
|
|
// resultUrl = yt.search.search(title + ' ' + artiste);
|
|
|
|
// resultUrlPool.add(resultUrl);
|
|
|
|
// resultUrl.then((value) {
|
|
// try {
|
|
// thisSong.url = value.first.url;
|
|
// } catch (e) {
|
|
// thisSong.url = '';
|
|
// }
|
|
// songsList.add(thisSong);
|
|
// });
|
|
}
|
|
|
|
// for (var searchTrack in resultUrlPool) {
|
|
// await searchTrack;
|
|
// }
|
|
|
|
songsList.sort((a, b) => a.number.compareTo(b.number));
|
|
|
|
return songsList;
|
|
}
|
|
|
|
TableRow _buildTableRow(Track track) {
|
|
final textStyle = TextStyle(
|
|
fontWeight: track.number == -1 ? FontWeight.w700 : FontWeight.normal,
|
|
color: Colors.grey[350]);
|
|
const rowPadding = EdgeInsets.all(10);
|
|
var yt = YoutubeExplode();
|
|
|
|
return TableRow(
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[850],
|
|
),
|
|
children: [
|
|
TableCell(
|
|
child: Padding(
|
|
padding: rowPadding,
|
|
child: InkWell(
|
|
onTap: () async {
|
|
if (track.id == null) {
|
|
final resultUrl = await yt.search
|
|
.search(track.title + ' ' + track.artiste);
|
|
track.id = resultUrl.first.id.value;
|
|
}
|
|
if (track.id != null) {
|
|
final response = await http.get(Uri.parse(
|
|
'https://ytdl.p2p.legal/ytdl.sh?${track.id}'));
|
|
final dlLink = response.body.replaceFirst('/dl/', '/play/');
|
|
print(dlLink);
|
|
player?.dispose();
|
|
Future.delayed(const Duration(milliseconds: 5));
|
|
player = Player.network(dlLink, autoPlay: true);
|
|
// player = Player.asset("assets/uka.mp3");
|
|
player!.play();
|
|
}
|
|
},
|
|
child: Text(track.title, style: textStyle),
|
|
)),
|
|
),
|
|
TableCell(
|
|
child: Padding(
|
|
padding: rowPadding,
|
|
child: Text(track.artiste, style: textStyle),
|
|
),
|
|
),
|
|
TableCell(
|
|
child: Padding(
|
|
padding: rowPadding,
|
|
child: Text(track.album ?? '', style: textStyle),
|
|
),
|
|
),
|
|
TableCell(
|
|
child: Padding(
|
|
padding: rowPadding,
|
|
child: InkWell(
|
|
onTap: () async {
|
|
if (track.id == null) {
|
|
final resultUrl = await yt.search
|
|
.search(track.title + ' ' + track.artiste);
|
|
track.id = resultUrl.first.id.value;
|
|
}
|
|
if (track.id != null) {
|
|
final response = await http.get(Uri.parse(
|
|
'https://ytdl.p2p.legal/ytdl.sh?${track.id}'));
|
|
final dlLink = response.body;
|
|
if (await canLaunchUrl(Uri.parse(dlLink))) {
|
|
launchUrl(Uri.parse(dlLink));
|
|
}
|
|
Clipboard.setData(ClipboardData(text: dlLink));
|
|
}
|
|
},
|
|
child: Text('Télécharger', style: textStyle),
|
|
)),
|
|
),
|
|
]);
|
|
}
|
|
|
|
class MyHttpOverrides extends HttpOverrides {
|
|
@override
|
|
HttpClient createHttpClient(SecurityContext? context) {
|
|
return super.createHttpClient(context)
|
|
..badCertificateCallback =
|
|
(X509Certificate cert, String host, int port) => true;
|
|
}
|
|
}
|
|
|
|
TextStyle globalTextStyle = TextStyle(color: Colors.grey[350]);
|