mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 21:29:36 +02:00
150 lines
4.6 KiB
Dart
150 lines
4.6 KiB
Dart
import 'package:spotiflac_android/utils/string_utils.dart';
|
|
import 'package:timezone/data/latest.dart' as tz_data;
|
|
import 'package:timezone/timezone.dart' as tz;
|
|
|
|
/// Provider-neutral event metadata. Calendar labels always use the venue's
|
|
/// local time, never the listener's device time zone.
|
|
class ArtistConcert {
|
|
const ArtistConcert({
|
|
required this.id,
|
|
required this.location,
|
|
required this.venue,
|
|
required this.date,
|
|
required this.hasTime,
|
|
this.url,
|
|
this.detailId,
|
|
});
|
|
|
|
final String id;
|
|
final String location;
|
|
final String venue;
|
|
final DateTime date;
|
|
final bool hasTime;
|
|
final String? url;
|
|
final String? detailId;
|
|
|
|
static bool _timeZonesReady = false;
|
|
|
|
static List<ArtistConcert> parseList(Object? value) {
|
|
if (value is! List) return const [];
|
|
final concerts = <String, ArtistConcert>{};
|
|
for (final item in value.take(500)) {
|
|
if (item is! Map<String, dynamic>) continue;
|
|
String text(String key) =>
|
|
item[key] is String ? (item[key] as String).trim() : '';
|
|
final rawDate = text('start_at');
|
|
final match = RegExp(
|
|
r'^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::\d{2}(?:\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$',
|
|
).firstMatch(rawDate);
|
|
final instant = DateTime.tryParse(rawDate);
|
|
final location = text('location');
|
|
if (match == null || instant == null || location.isEmpty) continue;
|
|
final year = int.parse(match[1]!);
|
|
final month = int.parse(match[2]!);
|
|
final day = int.parse(match[3]!);
|
|
final hour = int.parse(match[4] ?? '0');
|
|
final minute = int.parse(match[5] ?? '0');
|
|
var date = DateTime.utc(year, month, day, hour, minute);
|
|
// DateTime.parse normalizes impossible dates; do not show invented days.
|
|
if (date.year != year ||
|
|
date.month != month ||
|
|
date.day != day ||
|
|
hour > 23 ||
|
|
minute > 59) {
|
|
continue;
|
|
}
|
|
final zone = text('time_zone');
|
|
if (zone.isNotEmpty && match[6] != null) {
|
|
if (!_timeZonesReady) {
|
|
tz_data.initializeTimeZones();
|
|
_timeZonesReady = true;
|
|
}
|
|
try {
|
|
date = tz.TZDateTime.from(instant, tz.getLocation(zone));
|
|
} on tz.LocationNotFoundException {
|
|
// Preserve the supplied wall-clock date if the zone is unknown.
|
|
}
|
|
}
|
|
final id = text('id');
|
|
final venue = text('venue');
|
|
final key = id.isEmpty ? '$rawDate|$location|$venue' : id;
|
|
concerts.putIfAbsent(
|
|
key,
|
|
() => ArtistConcert(
|
|
id: key,
|
|
location: location,
|
|
venue: venue,
|
|
date: date,
|
|
hasTime: match[4] != null,
|
|
url: normalizeRemoteHttpUrl(text('url')),
|
|
detailId: text('detail_id').isEmpty ? null : text('detail_id'),
|
|
),
|
|
);
|
|
}
|
|
return concerts.values.toList()..sort((a, b) => a.date.compareTo(b.date));
|
|
}
|
|
}
|
|
|
|
class ConcertDetail {
|
|
const ConcertDetail({
|
|
this.artistName,
|
|
this.title,
|
|
this.coverUrl,
|
|
this.venue,
|
|
this.address,
|
|
this.start,
|
|
this.end,
|
|
this.ticketUrl,
|
|
this.mapUrl,
|
|
this.url,
|
|
this.attribution,
|
|
this.setListId,
|
|
this.setListName,
|
|
this.setListCover,
|
|
});
|
|
|
|
final String? artistName;
|
|
final String? title;
|
|
final String? coverUrl;
|
|
final String? venue;
|
|
final String? address;
|
|
final DateTime? start;
|
|
final DateTime? end;
|
|
final String? ticketUrl;
|
|
final String? mapUrl;
|
|
final String? url;
|
|
final String? attribution;
|
|
final String? setListId;
|
|
final String? setListName;
|
|
final String? setListCover;
|
|
|
|
factory ConcertDetail.fromJson(Map<String, dynamic> json) {
|
|
String? text(Map<String, dynamic> data, String key) {
|
|
final value = data[key];
|
|
return value is String && value.trim().isNotEmpty ? value.trim() : null;
|
|
}
|
|
|
|
String? link(Map<String, dynamic> data, String key) =>
|
|
normalizeRemoteHttpUrl(text(data, key));
|
|
final setList = json['set_list'] is Map<String, dynamic>
|
|
? json['set_list'] as Map<String, dynamic>
|
|
: const <String, dynamic>{};
|
|
return ConcertDetail(
|
|
artistName: text(json, 'artist_name'),
|
|
title: text(json, 'title'),
|
|
coverUrl: link(json, 'cover_url'),
|
|
venue: text(json, 'venue'),
|
|
address: text(json, 'address'),
|
|
start: DateTime.tryParse(text(json, 'start_at') ?? ''),
|
|
end: DateTime.tryParse(text(json, 'end_at') ?? ''),
|
|
ticketUrl: link(json, 'ticket_url'),
|
|
mapUrl: link(json, 'map_url'),
|
|
url: link(json, 'url'),
|
|
attribution: text(json, 'attribution'),
|
|
setListId: text(setList, 'id'),
|
|
setListName: text(setList, 'name'),
|
|
setListCover: link(setList, 'cover_url'),
|
|
);
|
|
}
|
|
}
|