Implement some sort of basic rss scraping

This commit is contained in:
2025-02-22 14:25:48 +01:00
parent bc1e553fc7
commit 5ed90da80b
6 changed files with 350 additions and 21 deletions

79
lib/db.dart Normal file
View File

@@ -0,0 +1,79 @@
import 'dart:io' show Platform, Directory;
import 'package:path/path.dart' as path;
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
const settingsDir = '.gamer-updater';
const dbFileName = 'data.db';
class DB {
static late Database db;
// SQL schema definition
static const String _schema = '''
CREATE TABLE IF NOT EXISTS games (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
actual_version TEXT NOT NULL,
last_played TEXT NOT NULL,
rss_feed_url TEXT NOT NULL,
version_regex TEXT NOT NULL,
last_updated TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_games_name ON games (name);
CREATE UNIQUE INDEX IF NOT EXISTS idx_games_name_unique ON games (name);
''';
static Future<String> _getDatabasePath() async {
print('Attempting to get database path...');
if (Platform.isWindows || Platform.isLinux) {
// Get user's home directory
final home =
Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
if (home == null) {
throw Exception('Could not find home directory');
}
print('Home directory found: home');
final dbDir = Directory(path.join(home, settingsDir));
if (!await dbDir.exists()) {
await dbDir.create(recursive: true);
print('$settingsDir directory created');
} else {
print('$settingsDir directory already exists');
}
return path.join(dbDir.path, dbFileName);
} else {
// Default path for other platforms
final databasesPath = await databaseFactoryFfi.getDatabasesPath();
print('Using default databases path: databasesPath');
return path.join(databasesPath, dbFileName);
}
}
static Future<void> init() async {
print('Starting database initialization...');
sqfliteFfiInit();
final dbPath = await _getDatabasePath();
print('Database path: dbPath');
try {
db = await databaseFactoryFfi.openDatabase(
dbPath,
options: OpenDatabaseOptions(
version: 1,
onCreate: (db, version) async {
print('Creating database schema...');
await db.execute(_schema);
print('Database schema created successfully');
},
),
);
print('Database opened and initialized');
} catch (e) {
print('Failed to initialize database: e');
rethrow;
}
}
}

88
lib/game.dart Normal file
View File

@@ -0,0 +1,88 @@
import 'package:gamer_updater/db.dart';
import 'package:gamer_updater/utils.dart';
import 'package:http/http.dart' as http;
import 'package:dart_rss/dart_rss.dart';
class Game {
final String name;
final String versionRegex;
final RegExp _internalVersionRegex;
final String lastPlayed;
String actualVersion;
String lastUpdated;
final String rssFeedUrl;
Game({
required this.name,
required this.versionRegex,
required this.lastPlayed,
this.actualVersion = '',
required this.rssFeedUrl,
this.lastUpdated = '',
}) : _internalVersionRegex = RegExp(versionRegex);
Future<void> updateActualVersion() async {
final response = await http.get(Uri.parse(rssFeedUrl));
final document = RssFeed.parse(response.body);
final pages = document.items;
pages.sort((a, b) {
var lhs = parseRfc822Date(a.pubDate!);
var rhs = parseRfc822Date(b.pubDate!);
return rhs.compareTo(lhs);
});
final versions =
pages
.map((e) => _internalVersionRegex.firstMatch(e.title!)?.group(1))
.toList();
for (int i = 0; i < versions.length; i++) {
final version = versions[i];
final page = pages[i];
if (version != null) {
actualVersion = version;
lastUpdated = parseRfc822Date(page.pubDate!).toIso8601String();
break;
}
}
}
}
class GameRepository {
static Future<Game> upsert(Game game) async {
final db = await DB.db;
await db.rawInsert(
'''
INSERT INTO games
(name, actual_version, last_played, rss_feed_url, version_regex, last_updated)
VALUES
(?, ?, ?, ?, ?, ?)
ON CONFLICT(name) DO UPDATE SET
actual_version = excluded.actual_version,
last_played = excluded.last_played,
rss_feed_url = excluded.rss_feed_url,
version_regex = excluded.version_regex,
last_updated = excluded.last_updated
''',
[
game.name,
game.actualVersion,
game.lastPlayed,
game.rssFeedUrl,
game.versionRegex,
game.lastUpdated,
],
);
return game;
}
}
//CREATE TABLE IF NOT EXISTS games (
// id INTEGER PRIMARY KEY AUTOINCREMENT,
// name TEXT NOT NULL,
// actual_version TEXT NOT NULL,
// last_played TEXT NOT NULL,
// rss_feed_url TEXT NOT NULL,
// version_regex TEXT NOT NULL,
// last_updated TEXT NOT NULL
//);
//CREATE INDEX IF NOT EXISTS idx_games_name ON games (name);

View File

@@ -1,6 +1,20 @@
import 'package:flutter/material.dart';
import 'package:gamer_updater/db.dart';
import 'package:gamer_updater/game.dart';
void main() async {
await DB.init();
var game = Game(
name: 'Rimworld',
versionRegex: r'(\d+\.\d+\.\d+)',
lastPlayed: '1.4.3704',
rssFeedUrl:
'https://store.steampowered.com/feeds/news/app/294100/?cc=HR&l=english',
);
await game.updateActualVersion();
await GameRepository.upsert(game);
void main() {
runApp(const MyApp());
}
@@ -14,6 +28,19 @@ class MyApp extends StatelessWidget {
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
darkTheme: ThemeData(
brightness: Brightness.dark,
useMaterial3: true,
scaffoldBackgroundColor: Colors.black,
colorSchemeSeed: Colors.black,
highlightColor: Colors.deepPurple,
textTheme: TextTheme(
bodyLarge: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
bodyMedium: TextStyle(fontSize: 20),
bodySmall: TextStyle(fontSize: 16),
),
),
themeMode: ThemeMode.system,
home: const MyHomePage(title: 'Gamer Updater'),
);
}
@@ -28,14 +55,6 @@ class MyHomePage extends StatefulWidget {
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -46,20 +65,9 @@ class _MyHomePageState extends State<MyHomePage> {
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
children: <Widget>[const Text('Hello cyka')],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

47
lib/utils.dart Normal file
View File

@@ -0,0 +1,47 @@
DateTime parseRfc822Date(String date) {
// Split the date string into components
final parts = date.split(' ');
// Map the month names to their corresponding numbers
const monthMap = {
'Jan': 1,
'Feb': 2,
'Mar': 3,
'Apr': 4,
'May': 5,
'Jun': 6,
'Jul': 7,
'Aug': 8,
'Sep': 9,
'Oct': 10,
'Nov': 11,
'Dec': 12,
};
// Extract the components
final day = int.parse(parts[1]);
final month = monthMap[parts[2]]!;
final year = int.parse(parts[3]);
final timeParts = parts[4].split(':');
final hour = int.parse(timeParts[0]);
final minute = int.parse(timeParts[1]);
final second = int.parse(timeParts[2]);
// Handle the timezone offset
final timezone = parts[5];
final isNegative = timezone.startsWith('-');
final tzHours = int.parse(timezone.substring(1, 3));
final tzMinutes = int.parse(timezone.substring(3, 5));
// Create the DateTime object
DateTime dateTime = DateTime(year, month, day, hour, minute, second);
// Adjust for timezone
if (isNegative) {
dateTime = dateTime.subtract(Duration(hours: tzHours, minutes: tzMinutes));
} else {
dateTime = dateTime.add(Duration(hours: tzHours, minutes: tzMinutes));
}
return dateTime;
}