ponshu-room-lite/lib/services/sakenowa_service.dart

208 lines
6.5 KiB
Dart
Raw Permalink Normal View History

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/sakenowa/sakenowa_models.dart';
/// さけのわAPI クライアントサービス
///
/// API Documentation: https://muro.sakenowa.com/sakenowa-data/api/
/// 利用規約: 無料・商用OK、帰属表示必須"Powered by さけのわデータ"
class SakenowaService {
static const _baseUrl = 'https://muro.sakenowa.com/sakenowa-data/api';
// キャッシュ(メモリ内)
List<SakenowaBrand>? _brandsCache;
List<SakenowaBrewery>? _breweriesCache;
List<SakenowaArea>? _areasCache;
List<SakenowaFlavorChart>? _flavorChartsCache;
DateTime? _lastFetch;
// キャッシュ有効期限24時間
static const _cacheDuration = Duration(hours: 24);
/// 全銘柄データを取得
Future<List<SakenowaBrand>> getBrands({bool forceRefresh = false}) async {
if (!forceRefresh && _brandsCache != null && _isCacheValid()) {
return _brandsCache!;
}
final response = await http.get(Uri.parse('$_baseUrl/brands'));
if (response.statusCode != 200) {
throw Exception('Failed to fetch brands: ${response.statusCode}');
}
final data = jsonDecode(response.body);
_brandsCache = (data['brands'] as List)
.map((e) => SakenowaBrand.fromJson(e))
.toList();
_lastFetch = DateTime.now();
return _brandsCache!;
}
/// 全蔵元データを取得
Future<List<SakenowaBrewery>> getBreweries({bool forceRefresh = false}) async {
if (!forceRefresh && _breweriesCache != null && _isCacheValid()) {
return _breweriesCache!;
}
final response = await http.get(Uri.parse('$_baseUrl/breweries'));
if (response.statusCode != 200) {
throw Exception('Failed to fetch breweries: ${response.statusCode}');
}
final data = jsonDecode(response.body);
_breweriesCache = (data['breweries'] as List)
.map((e) => SakenowaBrewery.fromJson(e))
.toList();
return _breweriesCache!;
}
/// 全地域データを取得
Future<List<SakenowaArea>> getAreas({bool forceRefresh = false}) async {
if (!forceRefresh && _areasCache != null && _isCacheValid()) {
return _areasCache!;
}
final response = await http.get(Uri.parse('$_baseUrl/areas'));
if (response.statusCode != 200) {
throw Exception('Failed to fetch areas: ${response.statusCode}');
}
final data = jsonDecode(response.body);
_areasCache = (data['areas'] as List)
.map((e) => SakenowaArea.fromJson(e))
.toList();
return _areasCache!;
}
/// 全フレーバーチャートを取得
Future<List<SakenowaFlavorChart>> getFlavorCharts({bool forceRefresh = false}) async {
if (!forceRefresh && _flavorChartsCache != null && _isCacheValid()) {
return _flavorChartsCache!;
}
final response = await http.get(Uri.parse('$_baseUrl/flavor-charts'));
if (response.statusCode != 200) {
throw Exception('Failed to fetch flavor charts: ${response.statusCode}');
}
final data = jsonDecode(response.body);
_flavorChartsCache = (data['flavorCharts'] as List)
.map((e) => SakenowaFlavorChart.fromJson(e))
.toList();
return _flavorChartsCache!;
}
/// 銘柄IDからフレーバーチャートを取得
Future<SakenowaFlavorChart?> getFlavorChartByBrandId(int brandId) async {
final charts = await getFlavorCharts();
return charts.cast<SakenowaFlavorChart?>().firstWhere(
(c) => c?.brandId == brandId,
orElse: () => null,
);
}
/// ランキングを取得(全国)
Future<List<SakenowaRanking>> getRankings() async {
try {
final response = await http.get(Uri.parse('$_baseUrl/rankings'));
if (response.statusCode != 200) {
throw Exception('Failed to fetch rankings: ${response.statusCode}');
}
final data = jsonDecode(response.body);
// APIレスポンスは 'overall' キーに全国ランキングが入っている
final overallList = data['overall'] as List?;
if (overallList == null) {
throw Exception('Rankings data not found in response');
}
return overallList
.map((e) => SakenowaRanking.fromJson(e))
.toList();
} catch (e) {
rethrow;
}
}
/// フレーバータグ一覧を取得
Future<List<SakenowaFlavorTag>> getFlavorTags() async {
final response = await http.get(Uri.parse('$_baseUrl/flavor-tags'));
if (response.statusCode != 200) {
throw Exception('Failed to fetch flavor tags: ${response.statusCode}');
}
final data = jsonDecode(response.body);
return (data['tags'] as List)
.map((e) => SakenowaFlavorTag.fromJson(e))
.toList();
}
/// 銘柄名で検索(部分一致)
Future<List<SakenowaBrand>> searchBrandsByName(String query) async {
final brands = await getBrands();
final normalizedQuery = query.toLowerCase();
return brands.where((brand) {
return brand.name.toLowerCase().contains(normalizedQuery);
}).toList();
}
/// 銘柄名から最も近いマッチを取得
Future<SakenowaBrand?> findBestMatch(String name) async {
final brands = await getBrands();
final normalizedName = name.toLowerCase().replaceAll(' ', '');
// 完全一致を優先
for (final brand in brands) {
if (brand.name.toLowerCase().replaceAll(' ', '') == normalizedName) {
return brand;
}
}
// 部分一致
for (final brand in brands) {
if (brand.name.toLowerCase().contains(normalizedName) ||
normalizedName.contains(brand.name.toLowerCase())) {
return brand;
}
}
return null;
}
/// 銘柄の蔵元情報を取得
Future<SakenowaBrewery?> getBreweryByBrandId(int brandId) async {
final brands = await getBrands();
final brand = brands.cast<SakenowaBrand?>().firstWhere(
(b) => b?.id == brandId,
orElse: () => null,
);
if (brand == null) return null;
final breweries = await getBreweries();
return breweries.cast<SakenowaBrewery?>().firstWhere(
(b) => b?.id == brand.breweryId,
orElse: () => null,
);
}
/// キャッシュが有効かチェック
bool _isCacheValid() {
if (_lastFetch == null) return false;
return DateTime.now().difference(_lastFetch!) < _cacheDuration;
}
/// キャッシュをクリア
void clearCache() {
_brandsCache = null;
_breweriesCache = null;
_areasCache = null;
_flavorChartsCache = null;
_lastFetch = null;
}
}