1202 lines
53 KiB
Java
1202 lines
53 KiB
Java
package cz.kamma.xtreamplayer;
|
|
|
|
import org.apache.logging.log4j.LogManager;
|
|
import org.apache.logging.log4j.Logger;
|
|
|
|
import java.io.IOException;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.security.MessageDigest;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.sql.Connection;
|
|
import java.sql.DriverManager;
|
|
import java.sql.PreparedStatement;
|
|
import java.sql.ResultSet;
|
|
import java.sql.SQLException;
|
|
import java.sql.Statement;
|
|
import java.sql.Timestamp;
|
|
import java.util.ArrayList;
|
|
import java.util.Base64;
|
|
import java.util.Comparator;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Optional;
|
|
|
|
/**
|
|
* Unified Data Access Object for both user credentials and library/stream data.
|
|
* Manages persistent data in H2 database for users, categories, streams, series, and favorites.
|
|
*/
|
|
final class ApplicationDao {
|
|
private static final Logger LOGGER = LogManager.getLogger(ApplicationDao.class);
|
|
private final Path dbPath;
|
|
private final String jdbcUrl;
|
|
|
|
public ApplicationDao(Path dbPath) {
|
|
this.dbPath = dbPath;
|
|
this.jdbcUrl = "jdbc:h2:file:" + dbPath.toAbsolutePath().toString().replace("\\", "/") + ";AUTO_SERVER=TRUE";
|
|
}
|
|
|
|
public void initialize() {
|
|
try {
|
|
Files.createDirectories(dbPath.getParent());
|
|
} catch (IOException e) {
|
|
LOGGER.error("Failed to create database directory", e);
|
|
throw new RuntimeException(e);
|
|
}
|
|
try {
|
|
try (Connection connection = openConnection(); Statement statement = connection.createStatement()) {
|
|
// User tables
|
|
statement.execute("""
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id IDENTITY PRIMARY KEY,
|
|
username VARCHAR(120) UNIQUE NOT NULL,
|
|
password_hash VARCHAR(256) NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
""");
|
|
// Library metadata
|
|
statement.execute("""
|
|
CREATE TABLE IF NOT EXISTS source_meta (
|
|
meta_key VARCHAR(120) PRIMARY KEY,
|
|
meta_value CLOB
|
|
)
|
|
""");
|
|
// Categories
|
|
statement.execute("""
|
|
CREATE TABLE IF NOT EXISTS live_categories (
|
|
category_id VARCHAR(80) PRIMARY KEY,
|
|
category_name VARCHAR(400) NOT NULL
|
|
)
|
|
""");
|
|
statement.execute("""
|
|
CREATE TABLE IF NOT EXISTS vod_categories (
|
|
category_id VARCHAR(80) PRIMARY KEY,
|
|
category_name VARCHAR(400) NOT NULL
|
|
)
|
|
""");
|
|
statement.execute("""
|
|
CREATE TABLE IF NOT EXISTS series_categories (
|
|
category_id VARCHAR(80) PRIMARY KEY,
|
|
category_name VARCHAR(400) NOT NULL
|
|
)
|
|
""");
|
|
// Streams
|
|
statement.execute("""
|
|
CREATE TABLE IF NOT EXISTS live_streams (
|
|
stream_id VARCHAR(120) PRIMARY KEY,
|
|
name VARCHAR(500) NOT NULL,
|
|
category_id VARCHAR(80),
|
|
epg_channel_id VARCHAR(180)
|
|
)
|
|
""");
|
|
statement.execute("""
|
|
CREATE TABLE IF NOT EXISTS vod_streams (
|
|
stream_id VARCHAR(120) PRIMARY KEY,
|
|
name VARCHAR(500) NOT NULL,
|
|
category_id VARCHAR(80),
|
|
container_extension VARCHAR(40)
|
|
)
|
|
""");
|
|
// Series
|
|
statement.execute("""
|
|
CREATE TABLE IF NOT EXISTS series_items (
|
|
series_id VARCHAR(120) PRIMARY KEY,
|
|
name VARCHAR(500) NOT NULL,
|
|
category_id VARCHAR(80)
|
|
)
|
|
""");
|
|
statement.execute("""
|
|
CREATE TABLE IF NOT EXISTS series_episodes (
|
|
episode_id VARCHAR(120) PRIMARY KEY,
|
|
series_id VARCHAR(120) NOT NULL,
|
|
season VARCHAR(40),
|
|
episode_num VARCHAR(40),
|
|
title VARCHAR(500),
|
|
container_extension VARCHAR(40)
|
|
)
|
|
""");
|
|
// Favorites
|
|
statement.execute("""
|
|
CREATE TABLE IF NOT EXISTS favorites (
|
|
favorite_key VARCHAR(260) PRIMARY KEY,
|
|
mode VARCHAR(80) NOT NULL,
|
|
ref_id VARCHAR(180),
|
|
ext VARCHAR(60),
|
|
title VARCHAR(500) NOT NULL,
|
|
category_id VARCHAR(120),
|
|
series_id VARCHAR(120),
|
|
season VARCHAR(40),
|
|
episode VARCHAR(40),
|
|
url CLOB,
|
|
created_at BIGINT NOT NULL
|
|
)
|
|
""");
|
|
// Indexes
|
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_live_streams_category ON live_streams(category_id)");
|
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_vod_streams_category ON vod_streams(category_id)");
|
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_series_items_category ON series_items(category_id)");
|
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_series_episodes_series ON series_episodes(series_id)");
|
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_series_episodes_series_sort ON series_episodes(series_id, season, episode_num, title)");
|
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_live_categories_name ON live_categories(category_name)");
|
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_vod_categories_name ON vod_categories(category_name)");
|
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_series_categories_name ON series_categories(category_name)");
|
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_live_streams_name ON live_streams(name)");
|
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_vod_streams_name ON vod_streams(name)");
|
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_series_items_name ON series_items(name)");
|
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_favorites_created_at ON favorites(created_at DESC)");
|
|
LOGGER.info("Database tables and indexes initialized at {}", dbPath);
|
|
}
|
|
try (Connection connection = openConnection()) {
|
|
initializeFullText(connection);
|
|
}
|
|
LOGGER.info("H2 repository initialized at {}", dbPath);
|
|
} catch (SQLException exception) {
|
|
LOGGER.error("Failed to initialize database", exception);
|
|
throw new RuntimeException(exception);
|
|
}
|
|
}
|
|
|
|
// ===== USER MANAGEMENT =====
|
|
|
|
public void createUser(String username, String password) {
|
|
if (username == null || username.isBlank() || password == null || password.isBlank()) {
|
|
throw new IllegalArgumentException("Username and password must not be null or empty");
|
|
}
|
|
|
|
try (Connection connection = openConnection()) {
|
|
String sql = "INSERT INTO users (username, password_hash) VALUES (?, ?)";
|
|
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
|
stmt.setString(1, username);
|
|
stmt.setString(2, hashPassword(password));
|
|
stmt.executeUpdate();
|
|
LOGGER.info("User created: {}", username);
|
|
}
|
|
} catch (SQLException exception) {
|
|
if (exception.getMessage().contains("Unique constraint")) {
|
|
throw new IllegalArgumentException("User '" + username + "' already exists");
|
|
}
|
|
LOGGER.error("Failed to create user: {}", username, exception);
|
|
throw new RuntimeException(exception);
|
|
}
|
|
}
|
|
|
|
public void updatePassword(String username, String newPassword) {
|
|
if (username == null || username.isBlank() || newPassword == null || newPassword.isBlank()) {
|
|
throw new IllegalArgumentException("Username and password must not be null or empty");
|
|
}
|
|
|
|
try (Connection connection = openConnection()) {
|
|
String sql = "UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE username = ?";
|
|
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
|
stmt.setString(1, hashPassword(newPassword));
|
|
stmt.setString(2, username);
|
|
int updated = stmt.executeUpdate();
|
|
if (updated == 0) {
|
|
throw new IllegalArgumentException("User '" + username + "' not found");
|
|
}
|
|
LOGGER.info("Password updated for user: {}", username);
|
|
}
|
|
} catch (SQLException exception) {
|
|
LOGGER.error("Failed to update password for user: {}", username, exception);
|
|
throw new RuntimeException(exception);
|
|
}
|
|
}
|
|
|
|
public void deleteUser(String username) {
|
|
if (username == null || username.isBlank()) {
|
|
throw new IllegalArgumentException("Username must not be null or empty");
|
|
}
|
|
|
|
try (Connection connection = openConnection()) {
|
|
String sql = "DELETE FROM users WHERE username = ?";
|
|
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
|
stmt.setString(1, username);
|
|
int deleted = stmt.executeUpdate();
|
|
if (deleted == 0) {
|
|
throw new IllegalArgumentException("User '" + username + "' not found");
|
|
}
|
|
LOGGER.info("User deleted: {}", username);
|
|
}
|
|
} catch (SQLException exception) {
|
|
LOGGER.error("Failed to delete user: {}", username, exception);
|
|
throw new RuntimeException(exception);
|
|
}
|
|
}
|
|
|
|
public Optional<User> getUser(String username) {
|
|
if (username == null || username.isBlank()) {
|
|
return Optional.empty();
|
|
}
|
|
|
|
try (Connection connection = openConnection()) {
|
|
String sql = "SELECT id, username, password_hash, created_at, updated_at FROM users WHERE username = ?";
|
|
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
|
stmt.setString(1, username);
|
|
try (ResultSet rs = stmt.executeQuery()) {
|
|
if (rs.next()) {
|
|
return Optional.of(new User(
|
|
rs.getInt("id"),
|
|
rs.getString("username"),
|
|
rs.getString("password_hash"),
|
|
rs.getTimestamp("created_at"),
|
|
rs.getTimestamp("updated_at")
|
|
));
|
|
}
|
|
}
|
|
}
|
|
} catch (SQLException exception) {
|
|
LOGGER.error("Failed to get user: {}", username, exception);
|
|
}
|
|
return Optional.empty();
|
|
}
|
|
|
|
public List<User> getAllUsers() {
|
|
List<User> users = new ArrayList<>();
|
|
try (Connection connection = openConnection()) {
|
|
String sql = "SELECT id, username, password_hash, created_at, updated_at FROM users ORDER BY username";
|
|
try (Statement stmt = connection.createStatement();
|
|
ResultSet rs = stmt.executeQuery(sql)) {
|
|
while (rs.next()) {
|
|
users.add(new User(
|
|
rs.getInt("id"),
|
|
rs.getString("username"),
|
|
rs.getString("password_hash"),
|
|
rs.getTimestamp("created_at"),
|
|
rs.getTimestamp("updated_at")
|
|
));
|
|
}
|
|
}
|
|
} catch (SQLException exception) {
|
|
LOGGER.error("Failed to get all users", exception);
|
|
}
|
|
return users;
|
|
}
|
|
|
|
public boolean verifyPassword(String username, String password) {
|
|
Optional<User> user = getUser(username);
|
|
if (user.isEmpty()) {
|
|
return false;
|
|
}
|
|
return hashPassword(password).equals(user.get().getPasswordHash());
|
|
}
|
|
|
|
public boolean userExists(String username) {
|
|
return getUser(username).isPresent();
|
|
}
|
|
|
|
// ===== LIBRARY MANAGEMENT =====
|
|
|
|
void clearAllSources() {
|
|
LOGGER.info("Clearing all cached library data in H2");
|
|
inTransaction(connection -> {
|
|
try (Statement statement = connection.createStatement()) {
|
|
statement.executeUpdate("DELETE FROM live_categories");
|
|
statement.executeUpdate("DELETE FROM vod_categories");
|
|
statement.executeUpdate("DELETE FROM series_categories");
|
|
statement.executeUpdate("DELETE FROM live_streams");
|
|
statement.executeUpdate("DELETE FROM vod_streams");
|
|
statement.executeUpdate("DELETE FROM series_items");
|
|
statement.executeUpdate("DELETE FROM series_episodes");
|
|
}
|
|
});
|
|
}
|
|
|
|
void replaceLiveCategories(List<CategoryRow> rows) {
|
|
replaceCategories("live_categories", rows);
|
|
}
|
|
|
|
void replaceVodCategories(List<CategoryRow> rows) {
|
|
replaceCategories("vod_categories", rows);
|
|
}
|
|
|
|
void replaceSeriesCategories(List<CategoryRow> rows) {
|
|
replaceCategories("series_categories", rows);
|
|
}
|
|
|
|
private void replaceCategories(String tableName, List<CategoryRow> rows) {
|
|
LOGGER.info("Replacing {} with {} rows", tableName, rows.size());
|
|
inTransaction(connection -> {
|
|
try (Statement statement = connection.createStatement()) {
|
|
statement.executeUpdate("DELETE FROM " + tableName);
|
|
}
|
|
try (PreparedStatement preparedStatement = connection.prepareStatement(
|
|
"MERGE INTO " + tableName + " (category_id, category_name) KEY(category_id) VALUES (?, ?)")) {
|
|
for (CategoryRow row : rows) {
|
|
preparedStatement.setString(1, row.categoryId());
|
|
preparedStatement.setString(2, row.categoryName());
|
|
preparedStatement.addBatch();
|
|
}
|
|
preparedStatement.executeBatch();
|
|
}
|
|
});
|
|
}
|
|
|
|
void replaceLiveStreams(List<LiveStreamRow> rows) {
|
|
LOGGER.info("Replacing live streams with {} rows", rows.size());
|
|
inTransaction(connection -> {
|
|
try (Statement statement = connection.createStatement()) {
|
|
statement.executeUpdate("DELETE FROM live_streams");
|
|
}
|
|
try (PreparedStatement preparedStatement = connection.prepareStatement(
|
|
"MERGE INTO live_streams (stream_id, name, category_id, epg_channel_id) KEY(stream_id) VALUES (?, ?, ?, ?)")) {
|
|
for (LiveStreamRow row : rows) {
|
|
preparedStatement.setString(1, row.streamId());
|
|
preparedStatement.setString(2, row.name());
|
|
preparedStatement.setString(3, row.categoryId());
|
|
preparedStatement.setString(4, row.epgChannelId());
|
|
preparedStatement.addBatch();
|
|
}
|
|
preparedStatement.executeBatch();
|
|
}
|
|
});
|
|
}
|
|
|
|
void replaceVodStreams(List<VodStreamRow> rows) {
|
|
LOGGER.info("Replacing VOD streams with {} rows", rows.size());
|
|
inTransaction(connection -> {
|
|
try (Statement statement = connection.createStatement()) {
|
|
statement.executeUpdate("DELETE FROM vod_streams");
|
|
}
|
|
try (PreparedStatement preparedStatement = connection.prepareStatement(
|
|
"MERGE INTO vod_streams (stream_id, name, category_id, container_extension) KEY(stream_id) VALUES (?, ?, ?, ?)")) {
|
|
for (VodStreamRow row : rows) {
|
|
preparedStatement.setString(1, row.streamId());
|
|
preparedStatement.setString(2, row.name());
|
|
preparedStatement.setString(3, row.categoryId());
|
|
preparedStatement.setString(4, row.containerExtension());
|
|
preparedStatement.addBatch();
|
|
}
|
|
preparedStatement.executeBatch();
|
|
}
|
|
});
|
|
}
|
|
|
|
void replaceSeriesItems(List<SeriesItemRow> rows) {
|
|
LOGGER.info("Replacing series items with {} rows", rows.size());
|
|
inTransaction(connection -> {
|
|
try (Statement statement = connection.createStatement()) {
|
|
statement.executeUpdate("DELETE FROM series_items");
|
|
}
|
|
try (PreparedStatement preparedStatement = connection.prepareStatement(
|
|
"MERGE INTO series_items (series_id, name, category_id) KEY(series_id) VALUES (?, ?, ?)")) {
|
|
for (SeriesItemRow row : rows) {
|
|
preparedStatement.setString(1, row.seriesId());
|
|
preparedStatement.setString(2, row.name());
|
|
preparedStatement.setString(3, row.categoryId());
|
|
preparedStatement.addBatch();
|
|
}
|
|
preparedStatement.executeBatch();
|
|
}
|
|
});
|
|
}
|
|
|
|
void replaceSeriesEpisodes(String seriesId, List<SeriesEpisodeRow> rows) {
|
|
LOGGER.info("Replacing series episodes for series_id={} with {} rows", seriesId, rows.size());
|
|
inTransaction(connection -> {
|
|
try (PreparedStatement deleteStatement = connection.prepareStatement(
|
|
"DELETE FROM series_episodes WHERE series_id = ?")) {
|
|
deleteStatement.setString(1, seriesId);
|
|
deleteStatement.executeUpdate();
|
|
}
|
|
try (PreparedStatement preparedStatement = connection.prepareStatement(
|
|
"MERGE INTO series_episodes (episode_id, series_id, season, episode_num, title, container_extension) KEY(episode_id) VALUES (?, ?, ?, ?, ?, ?)")) {
|
|
for (SeriesEpisodeRow row : rows) {
|
|
preparedStatement.setString(1, row.episodeId());
|
|
preparedStatement.setString(2, row.seriesId());
|
|
preparedStatement.setString(3, row.season());
|
|
preparedStatement.setString(4, row.episodeNum());
|
|
preparedStatement.setString(5, row.title());
|
|
preparedStatement.setString(6, row.containerExtension());
|
|
preparedStatement.addBatch();
|
|
}
|
|
preparedStatement.executeBatch();
|
|
}
|
|
});
|
|
}
|
|
|
|
List<CategoryRow> listCategories(String type) {
|
|
String table = switch (normalizeType(type)) {
|
|
case "live" -> "live_categories";
|
|
case "vod" -> "vod_categories";
|
|
case "series" -> "series_categories";
|
|
default -> throw new IllegalArgumentException("Unsupported type: " + type);
|
|
};
|
|
|
|
List<CategoryRow> rows = new ArrayList<>();
|
|
try (Connection connection = openConnection();
|
|
PreparedStatement preparedStatement = connection.prepareStatement(
|
|
"SELECT category_id, category_name FROM " + table + " ORDER BY LOWER(category_name), category_name")) {
|
|
try (ResultSet resultSet = preparedStatement.executeQuery()) {
|
|
while (resultSet.next()) {
|
|
rows.add(new CategoryRow(
|
|
resultSet.getString("category_id"),
|
|
resultSet.getString("category_name")
|
|
));
|
|
}
|
|
}
|
|
return rows;
|
|
} catch (SQLException exception) {
|
|
throw new IllegalStateException("Unable to list categories.", exception);
|
|
}
|
|
}
|
|
|
|
List<LiveStreamRow> listLiveStreams(String categoryId, String search, Integer limit, Integer offset) {
|
|
StringBuilder sql = new StringBuilder(
|
|
"SELECT stream_id, name, category_id, epg_channel_id FROM live_streams WHERE 1=1");
|
|
List<String> args = new ArrayList<>();
|
|
if (categoryId != null && !categoryId.isBlank()) {
|
|
sql.append(" AND category_id = ?");
|
|
args.add(categoryId.trim());
|
|
}
|
|
if (search != null && !search.isBlank()) {
|
|
sql.append(" AND LOWER(name) LIKE ?");
|
|
args.add("%" + search.trim().toLowerCase(Locale.ROOT) + "%");
|
|
}
|
|
sql.append(" ORDER BY LOWER(name), name");
|
|
if (limit != null && limit > 0) {
|
|
sql.append(" LIMIT ").append(limit);
|
|
if (offset != null && offset > 0) {
|
|
sql.append(" OFFSET ").append(offset);
|
|
}
|
|
}
|
|
|
|
List<LiveStreamRow> rows = new ArrayList<>();
|
|
try (Connection connection = openConnection();
|
|
PreparedStatement preparedStatement = connection.prepareStatement(sql.toString())) {
|
|
bindStringArgs(preparedStatement, args);
|
|
try (ResultSet resultSet = preparedStatement.executeQuery()) {
|
|
while (resultSet.next()) {
|
|
rows.add(new LiveStreamRow(
|
|
resultSet.getString("stream_id"),
|
|
resultSet.getString("name"),
|
|
resultSet.getString("category_id"),
|
|
resultSet.getString("epg_channel_id")
|
|
));
|
|
}
|
|
}
|
|
return rows;
|
|
} catch (SQLException exception) {
|
|
throw new IllegalStateException("Unable to list live streams.", exception);
|
|
}
|
|
}
|
|
|
|
List<VodStreamRow> listVodStreams(String categoryId, String search, Integer limit, Integer offset) {
|
|
StringBuilder sql = new StringBuilder(
|
|
"SELECT stream_id, name, category_id, container_extension FROM vod_streams WHERE 1=1");
|
|
List<String> args = new ArrayList<>();
|
|
if (categoryId != null && !categoryId.isBlank()) {
|
|
sql.append(" AND category_id = ?");
|
|
args.add(categoryId.trim());
|
|
}
|
|
if (search != null && !search.isBlank()) {
|
|
sql.append(" AND LOWER(name) LIKE ?");
|
|
args.add("%" + search.trim().toLowerCase(Locale.ROOT) + "%");
|
|
}
|
|
sql.append(" ORDER BY LOWER(name), name");
|
|
if (limit != null && limit > 0) {
|
|
sql.append(" LIMIT ").append(limit);
|
|
if (offset != null && offset > 0) {
|
|
sql.append(" OFFSET ").append(offset);
|
|
}
|
|
}
|
|
|
|
List<VodStreamRow> rows = new ArrayList<>();
|
|
try (Connection connection = openConnection();
|
|
PreparedStatement preparedStatement = connection.prepareStatement(sql.toString())) {
|
|
bindStringArgs(preparedStatement, args);
|
|
try (ResultSet resultSet = preparedStatement.executeQuery()) {
|
|
while (resultSet.next()) {
|
|
rows.add(new VodStreamRow(
|
|
resultSet.getString("stream_id"),
|
|
resultSet.getString("name"),
|
|
resultSet.getString("category_id"),
|
|
resultSet.getString("container_extension")
|
|
));
|
|
}
|
|
}
|
|
return rows;
|
|
} catch (SQLException exception) {
|
|
throw new IllegalStateException("Unable to list VOD streams.", exception);
|
|
}
|
|
}
|
|
|
|
List<SeriesItemRow> listSeriesItems(String categoryId, String search, Integer limit, Integer offset) {
|
|
StringBuilder sql = new StringBuilder(
|
|
"SELECT series_id, name, category_id FROM series_items WHERE 1=1");
|
|
List<String> args = new ArrayList<>();
|
|
if (categoryId != null && !categoryId.isBlank()) {
|
|
sql.append(" AND category_id = ?");
|
|
args.add(categoryId.trim());
|
|
}
|
|
if (search != null && !search.isBlank()) {
|
|
sql.append(" AND LOWER(name) LIKE ?");
|
|
args.add("%" + search.trim().toLowerCase(Locale.ROOT) + "%");
|
|
}
|
|
sql.append(" ORDER BY LOWER(name), name");
|
|
if (limit != null && limit > 0) {
|
|
sql.append(" LIMIT ").append(limit);
|
|
if (offset != null && offset > 0) {
|
|
sql.append(" OFFSET ").append(offset);
|
|
}
|
|
}
|
|
|
|
List<SeriesItemRow> rows = new ArrayList<>();
|
|
try (Connection connection = openConnection();
|
|
PreparedStatement preparedStatement = connection.prepareStatement(sql.toString())) {
|
|
bindStringArgs(preparedStatement, args);
|
|
try (ResultSet resultSet = preparedStatement.executeQuery()) {
|
|
while (resultSet.next()) {
|
|
rows.add(new SeriesItemRow(
|
|
resultSet.getString("series_id"),
|
|
resultSet.getString("name"),
|
|
resultSet.getString("category_id")
|
|
));
|
|
}
|
|
}
|
|
return rows;
|
|
} catch (SQLException exception) {
|
|
throw new IllegalStateException("Unable to list series items.", exception);
|
|
}
|
|
}
|
|
|
|
List<SeriesEpisodeRow> listSeriesEpisodes(String seriesId) {
|
|
List<SeriesEpisodeRow> rows = new ArrayList<>();
|
|
try (Connection connection = openConnection();
|
|
PreparedStatement preparedStatement = connection.prepareStatement("""
|
|
SELECT episode_id, series_id, season, episode_num, title, container_extension
|
|
FROM series_episodes
|
|
WHERE series_id = ?
|
|
ORDER BY season, episode_num, title
|
|
""")) {
|
|
preparedStatement.setString(1, seriesId);
|
|
try (ResultSet resultSet = preparedStatement.executeQuery()) {
|
|
while (resultSet.next()) {
|
|
rows.add(new SeriesEpisodeRow(
|
|
resultSet.getString("episode_id"),
|
|
resultSet.getString("series_id"),
|
|
resultSet.getString("season"),
|
|
resultSet.getString("episode_num"),
|
|
resultSet.getString("title"),
|
|
resultSet.getString("container_extension")
|
|
));
|
|
}
|
|
}
|
|
return rows;
|
|
} catch (SQLException exception) {
|
|
throw new IllegalStateException("Unable to list series episodes.", exception);
|
|
}
|
|
}
|
|
|
|
List<GlobalSearchRow> globalSearch(String queryRaw, int limitRaw, int offsetRaw) {
|
|
String query = queryRaw == null ? "" : queryRaw.trim();
|
|
if (query.isBlank()) {
|
|
return List.of();
|
|
}
|
|
int limit = Math.max(1, Math.min(limitRaw, 500));
|
|
int offset = Math.max(0, offsetRaw);
|
|
List<GlobalSearchRow> rows = new ArrayList<>();
|
|
try (Connection connection = openConnection();
|
|
PreparedStatement preparedStatement = connection.prepareStatement(
|
|
"SELECT \"TABLE\", KEYS, SCORE FROM FT_SEARCH_DATA(?, ?, ?)")) {
|
|
preparedStatement.setString(1, query);
|
|
preparedStatement.setInt(2, limit);
|
|
preparedStatement.setInt(3, offset);
|
|
try (ResultSet resultSet = preparedStatement.executeQuery()) {
|
|
while (resultSet.next()) {
|
|
String tableName = resultSet.getString("TABLE");
|
|
String id = firstKeyValue(resultSet);
|
|
double score = resultSet.getDouble("SCORE");
|
|
if (tableName == null || tableName.isBlank() || id.isBlank()) {
|
|
continue;
|
|
}
|
|
GlobalSearchRow row = resolveGlobalSearchRow(connection, tableName, id, score);
|
|
if (row != null) {
|
|
rows.add(row);
|
|
}
|
|
}
|
|
}
|
|
rows.sort(Comparator
|
|
.comparing((GlobalSearchRow row) -> safeLower(row.title()))
|
|
.thenComparing(row -> safeLower(row.kind()))
|
|
.thenComparing(row -> safeLower(row.id())));
|
|
return rows;
|
|
} catch (SQLException exception) {
|
|
throw new IllegalStateException("Unable to search library.", exception);
|
|
}
|
|
}
|
|
|
|
void setMeta(String key, String value) {
|
|
try (Connection connection = openConnection();
|
|
PreparedStatement preparedStatement = connection.prepareStatement(
|
|
"MERGE INTO source_meta (meta_key, meta_value) KEY(meta_key) VALUES (?, ?)")) {
|
|
preparedStatement.setString(1, key);
|
|
preparedStatement.setString(2, value);
|
|
preparedStatement.executeUpdate();
|
|
} catch (SQLException exception) {
|
|
throw new IllegalStateException("Unable to set metadata.", exception);
|
|
}
|
|
}
|
|
|
|
List<FavoriteRow> listFavorites(String searchRaw, int limitRaw, int offsetRaw) {
|
|
String search = searchRaw == null ? "" : searchRaw.trim().toLowerCase(Locale.ROOT);
|
|
int limit = Math.max(1, limitRaw);
|
|
int offset = Math.max(0, offsetRaw);
|
|
List<FavoriteRow> rows = new ArrayList<>();
|
|
StringBuilder sql = new StringBuilder("""
|
|
SELECT favorite_key, mode, ref_id, ext, title, category_id, series_id, season, episode, url, created_at
|
|
FROM favorites
|
|
""");
|
|
List<Object> args = new ArrayList<>();
|
|
if (!search.isBlank()) {
|
|
sql.append("""
|
|
WHERE LOWER(title) LIKE ?
|
|
OR LOWER(mode) LIKE ?
|
|
OR LOWER(url) LIKE ?
|
|
OR LOWER(favorite_key) LIKE ?
|
|
OR (
|
|
mode = 'live_category'
|
|
AND EXISTS (
|
|
SELECT 1
|
|
FROM live_streams ls
|
|
WHERE ls.category_id = favorites.ref_id
|
|
AND (LOWER(ls.name) LIKE ? OR LOWER(ls.stream_id) LIKE ?)
|
|
)
|
|
)
|
|
OR (
|
|
mode = 'vod_category'
|
|
AND EXISTS (
|
|
SELECT 1
|
|
FROM vod_streams vs
|
|
WHERE vs.category_id = favorites.ref_id
|
|
AND (LOWER(vs.name) LIKE ? OR LOWER(vs.stream_id) LIKE ?)
|
|
)
|
|
)
|
|
OR (
|
|
mode = 'series_category'
|
|
AND EXISTS (
|
|
SELECT 1
|
|
FROM series_items si
|
|
WHERE si.category_id = favorites.ref_id
|
|
AND (LOWER(si.name) LIKE ? OR LOWER(si.series_id) LIKE ?)
|
|
)
|
|
)
|
|
""");
|
|
String value = "%" + search + "%";
|
|
args.add(value);
|
|
args.add(value);
|
|
args.add(value);
|
|
args.add(value);
|
|
args.add(value);
|
|
args.add(value);
|
|
args.add(value);
|
|
args.add(value);
|
|
args.add(value);
|
|
args.add(value);
|
|
}
|
|
sql.append(" ORDER BY created_at DESC, favorite_key LIMIT ? OFFSET ?");
|
|
try (Connection connection = openConnection();
|
|
PreparedStatement preparedStatement = connection.prepareStatement(sql.toString())) {
|
|
args.add(limit);
|
|
args.add(offset);
|
|
for (int i = 0; i < args.size(); i++) {
|
|
Object arg = args.get(i);
|
|
if (arg instanceof Integer intArg) {
|
|
preparedStatement.setInt(i + 1, intArg);
|
|
} else {
|
|
preparedStatement.setString(i + 1, String.valueOf(arg));
|
|
}
|
|
}
|
|
try (ResultSet resultSet = preparedStatement.executeQuery()) {
|
|
while (resultSet.next()) {
|
|
rows.add(new FavoriteRow(
|
|
resultSet.getString("favorite_key"),
|
|
resultSet.getString("mode"),
|
|
resultSet.getString("ref_id"),
|
|
resultSet.getString("ext"),
|
|
resultSet.getString("title"),
|
|
resultSet.getString("category_id"),
|
|
resultSet.getString("series_id"),
|
|
resultSet.getString("season"),
|
|
resultSet.getString("episode"),
|
|
resultSet.getString("url"),
|
|
resultSet.getLong("created_at")
|
|
));
|
|
}
|
|
}
|
|
return rows;
|
|
} catch (SQLException exception) {
|
|
throw new IllegalStateException("Unable to list favorites.", exception);
|
|
}
|
|
}
|
|
|
|
int countFavorites(String searchRaw) {
|
|
String search = searchRaw == null ? "" : searchRaw.trim().toLowerCase(Locale.ROOT);
|
|
StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM favorites");
|
|
List<String> args = new ArrayList<>();
|
|
if (!search.isBlank()) {
|
|
sql.append("""
|
|
WHERE LOWER(title) LIKE ?
|
|
OR LOWER(mode) LIKE ?
|
|
OR LOWER(url) LIKE ?
|
|
OR LOWER(favorite_key) LIKE ?
|
|
OR (
|
|
mode = 'live_category'
|
|
AND EXISTS (
|
|
SELECT 1
|
|
FROM live_streams ls
|
|
WHERE ls.category_id = favorites.ref_id
|
|
AND (LOWER(ls.name) LIKE ? OR LOWER(ls.stream_id) LIKE ?)
|
|
)
|
|
)
|
|
OR (
|
|
mode = 'vod_category'
|
|
AND EXISTS (
|
|
SELECT 1
|
|
FROM vod_streams vs
|
|
WHERE vs.category_id = favorites.ref_id
|
|
AND (LOWER(vs.name) LIKE ? OR LOWER(vs.stream_id) LIKE ?)
|
|
)
|
|
)
|
|
OR (
|
|
mode = 'series_category'
|
|
AND EXISTS (
|
|
SELECT 1
|
|
FROM series_items si
|
|
WHERE si.category_id = favorites.ref_id
|
|
AND (LOWER(si.name) LIKE ? OR LOWER(si.series_id) LIKE ?)
|
|
)
|
|
)
|
|
""");
|
|
String value = "%" + search + "%";
|
|
args.add(value);
|
|
args.add(value);
|
|
args.add(value);
|
|
args.add(value);
|
|
args.add(value);
|
|
args.add(value);
|
|
args.add(value);
|
|
args.add(value);
|
|
args.add(value);
|
|
args.add(value);
|
|
}
|
|
try (Connection connection = openConnection();
|
|
PreparedStatement preparedStatement = connection.prepareStatement(sql.toString())) {
|
|
for (int i = 0; i < args.size(); i++) {
|
|
preparedStatement.setString(i + 1, args.get(i));
|
|
}
|
|
try (ResultSet resultSet = preparedStatement.executeQuery()) {
|
|
if (resultSet.next()) {
|
|
return resultSet.getInt(1);
|
|
}
|
|
return 0;
|
|
}
|
|
} catch (SQLException exception) {
|
|
throw new IllegalStateException("Unable to count favorites.", exception);
|
|
}
|
|
}
|
|
|
|
void upsertFavorite(FavoriteRow row) {
|
|
try (Connection connection = openConnection();
|
|
PreparedStatement preparedStatement = connection.prepareStatement("""
|
|
MERGE INTO favorites
|
|
(favorite_key, mode, ref_id, ext, title, category_id, series_id, season, episode, url, created_at)
|
|
KEY(favorite_key)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""")) {
|
|
preparedStatement.setString(1, row.key());
|
|
preparedStatement.setString(2, row.mode());
|
|
preparedStatement.setString(3, row.id());
|
|
preparedStatement.setString(4, row.ext());
|
|
preparedStatement.setString(5, row.title());
|
|
preparedStatement.setString(6, row.categoryId());
|
|
preparedStatement.setString(7, row.seriesId());
|
|
preparedStatement.setString(8, row.season());
|
|
preparedStatement.setString(9, row.episode());
|
|
preparedStatement.setString(10, row.url());
|
|
preparedStatement.setLong(11, row.createdAt());
|
|
preparedStatement.executeUpdate();
|
|
} catch (SQLException exception) {
|
|
throw new IllegalStateException("Unable to save favorite.", exception);
|
|
}
|
|
}
|
|
|
|
boolean deleteFavorite(String key) {
|
|
try (Connection connection = openConnection();
|
|
PreparedStatement preparedStatement = connection.prepareStatement(
|
|
"DELETE FROM favorites WHERE favorite_key = ?")) {
|
|
preparedStatement.setString(1, key);
|
|
return preparedStatement.executeUpdate() > 0;
|
|
} catch (SQLException exception) {
|
|
throw new IllegalStateException("Unable to delete favorite.", exception);
|
|
}
|
|
}
|
|
|
|
String getMeta(String key) {
|
|
try (Connection connection = openConnection();
|
|
PreparedStatement preparedStatement = connection.prepareStatement(
|
|
"SELECT meta_value FROM source_meta WHERE meta_key = ?")) {
|
|
preparedStatement.setString(1, key);
|
|
try (ResultSet resultSet = preparedStatement.executeQuery()) {
|
|
if (resultSet.next()) {
|
|
return resultSet.getString("meta_value");
|
|
}
|
|
return null;
|
|
}
|
|
} catch (SQLException exception) {
|
|
throw new IllegalStateException("Unable to read metadata.", exception);
|
|
}
|
|
}
|
|
|
|
LibraryCounts countAll() {
|
|
try (Connection connection = openConnection()) {
|
|
return new LibraryCounts(
|
|
countTable(connection, "live_categories"),
|
|
countTable(connection, "live_streams"),
|
|
countTable(connection, "vod_categories"),
|
|
countTable(connection, "vod_streams"),
|
|
countTable(connection, "series_categories"),
|
|
countTable(connection, "series_items"),
|
|
countTable(connection, "series_episodes")
|
|
);
|
|
} catch (SQLException exception) {
|
|
throw new IllegalStateException("Unable to count library rows.", exception);
|
|
}
|
|
}
|
|
|
|
// ===== PRIVATE HELPERS =====
|
|
|
|
private int countTable(Connection connection, String tableName) throws SQLException {
|
|
try (Statement statement = connection.createStatement();
|
|
ResultSet resultSet = statement.executeQuery("SELECT COUNT(*) AS c FROM " + tableName)) {
|
|
resultSet.next();
|
|
return resultSet.getInt("c");
|
|
}
|
|
}
|
|
|
|
private void bindStringArgs(PreparedStatement preparedStatement, List<String> args) throws SQLException {
|
|
for (int index = 0; index < args.size(); index++) {
|
|
preparedStatement.setString(index + 1, args.get(index));
|
|
}
|
|
}
|
|
|
|
private void initializeFullText(Connection connection) throws SQLException {
|
|
try (Statement statement = connection.createStatement()) {
|
|
statement.execute("CREATE ALIAS IF NOT EXISTS FTL_INIT FOR 'org.h2.fulltext.FullText.init'");
|
|
statement.execute("CALL FTL_INIT()");
|
|
}
|
|
ensureFullTextIndex(connection, "LIVE_CATEGORIES", "CATEGORY_NAME");
|
|
ensureFullTextIndex(connection, "VOD_CATEGORIES", "CATEGORY_NAME");
|
|
ensureFullTextIndex(connection, "SERIES_CATEGORIES", "CATEGORY_NAME");
|
|
ensureFullTextIndex(connection, "LIVE_STREAMS", "STREAM_ID,NAME");
|
|
ensureFullTextIndex(connection, "VOD_STREAMS", "STREAM_ID,NAME");
|
|
ensureFullTextIndex(connection, "SERIES_ITEMS", "SERIES_ID,NAME");
|
|
}
|
|
|
|
private void ensureFullTextIndex(Connection connection, String tableName, String columnsCsv) throws SQLException {
|
|
try (PreparedStatement preparedStatement = connection.prepareStatement("""
|
|
SELECT COUNT(*)
|
|
FROM FT.INDEXES
|
|
WHERE SCHEMA = 'PUBLIC'
|
|
AND "TABLE" = ?
|
|
""")) {
|
|
preparedStatement.setString(1, tableName);
|
|
try (ResultSet resultSet = preparedStatement.executeQuery()) {
|
|
resultSet.next();
|
|
if (resultSet.getInt(1) > 0) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
try (PreparedStatement preparedStatement = connection.prepareStatement("CALL FT_CREATE_INDEX(?, ?, ?)")) {
|
|
preparedStatement.setString(1, "PUBLIC");
|
|
preparedStatement.setString(2, tableName);
|
|
preparedStatement.setString(3, columnsCsv);
|
|
preparedStatement.execute();
|
|
}
|
|
}
|
|
|
|
private String firstKeyValue(ResultSet resultSet) throws SQLException {
|
|
java.sql.Array array = resultSet.getArray("KEYS");
|
|
if (array == null) {
|
|
return "";
|
|
}
|
|
try {
|
|
Object raw = array.getArray();
|
|
if (raw instanceof Object[] values && values.length > 0 && values[0] != null) {
|
|
return String.valueOf(values[0]);
|
|
}
|
|
return "";
|
|
} finally {
|
|
array.free();
|
|
}
|
|
}
|
|
|
|
private GlobalSearchRow resolveGlobalSearchRow(Connection connection, String tableNameRaw, String id, double score)
|
|
throws SQLException {
|
|
String tableName = tableNameRaw.trim().toUpperCase(Locale.ROOT);
|
|
return switch (tableName) {
|
|
case "LIVE_CATEGORIES" -> findLiveCategoryHit(connection, id, score);
|
|
case "VOD_CATEGORIES" -> findVodCategoryHit(connection, id, score);
|
|
case "SERIES_CATEGORIES" -> findSeriesCategoryHit(connection, id, score);
|
|
case "LIVE_STREAMS" -> findLiveStreamHit(connection, id, score);
|
|
case "VOD_STREAMS" -> findVodStreamHit(connection, id, score);
|
|
case "SERIES_ITEMS" -> findSeriesItemHit(connection, id, score);
|
|
default -> null;
|
|
};
|
|
}
|
|
|
|
private GlobalSearchRow findLiveCategoryHit(Connection connection, String categoryId, double score) throws SQLException {
|
|
try (PreparedStatement preparedStatement = connection.prepareStatement("""
|
|
SELECT category_id, category_name
|
|
FROM live_categories
|
|
WHERE category_id = ?
|
|
""")) {
|
|
preparedStatement.setString(1, categoryId);
|
|
try (ResultSet resultSet = preparedStatement.executeQuery()) {
|
|
if (!resultSet.next()) {
|
|
return null;
|
|
}
|
|
String id = resultSet.getString("category_id");
|
|
String name = resultSet.getString("category_name");
|
|
return new GlobalSearchRow("live_category", id, name, id, name, "", "", score);
|
|
}
|
|
}
|
|
}
|
|
|
|
private GlobalSearchRow findVodCategoryHit(Connection connection, String categoryId, double score) throws SQLException {
|
|
try (PreparedStatement preparedStatement = connection.prepareStatement("""
|
|
SELECT category_id, category_name
|
|
FROM vod_categories
|
|
WHERE category_id = ?
|
|
""")) {
|
|
preparedStatement.setString(1, categoryId);
|
|
try (ResultSet resultSet = preparedStatement.executeQuery()) {
|
|
if (!resultSet.next()) {
|
|
return null;
|
|
}
|
|
String id = resultSet.getString("category_id");
|
|
String name = resultSet.getString("category_name");
|
|
return new GlobalSearchRow("vod_category", id, name, id, name, "", "", score);
|
|
}
|
|
}
|
|
}
|
|
|
|
private GlobalSearchRow findSeriesCategoryHit(Connection connection, String categoryId, double score) throws SQLException {
|
|
try (PreparedStatement preparedStatement = connection.prepareStatement("""
|
|
SELECT category_id, category_name
|
|
FROM series_categories
|
|
WHERE category_id = ?
|
|
""")) {
|
|
preparedStatement.setString(1, categoryId);
|
|
try (ResultSet resultSet = preparedStatement.executeQuery()) {
|
|
if (!resultSet.next()) {
|
|
return null;
|
|
}
|
|
String id = resultSet.getString("category_id");
|
|
String name = resultSet.getString("category_name");
|
|
return new GlobalSearchRow("series_category", id, name, id, name, "", "", score);
|
|
}
|
|
}
|
|
}
|
|
|
|
private GlobalSearchRow findLiveStreamHit(Connection connection, String streamId, double score) throws SQLException {
|
|
try (PreparedStatement preparedStatement = connection.prepareStatement("""
|
|
SELECT ls.stream_id, ls.name, ls.category_id, ls.epg_channel_id, lc.category_name
|
|
FROM live_streams ls
|
|
LEFT JOIN live_categories lc ON lc.category_id = ls.category_id
|
|
WHERE ls.stream_id = ?
|
|
""")) {
|
|
preparedStatement.setString(1, streamId);
|
|
try (ResultSet resultSet = preparedStatement.executeQuery()) {
|
|
if (!resultSet.next()) {
|
|
return null;
|
|
}
|
|
return new GlobalSearchRow(
|
|
"live",
|
|
resultSet.getString("stream_id"),
|
|
resultSet.getString("name"),
|
|
resultSet.getString("category_id"),
|
|
resultSet.getString("category_name"),
|
|
"",
|
|
resultSet.getString("epg_channel_id"),
|
|
score
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private GlobalSearchRow findVodStreamHit(Connection connection, String streamId, double score) throws SQLException {
|
|
try (PreparedStatement preparedStatement = connection.prepareStatement("""
|
|
SELECT vs.stream_id, vs.name, vs.category_id, vs.container_extension, vc.category_name
|
|
FROM vod_streams vs
|
|
LEFT JOIN vod_categories vc ON vc.category_id = vs.category_id
|
|
WHERE vs.stream_id = ?
|
|
""")) {
|
|
preparedStatement.setString(1, streamId);
|
|
try (ResultSet resultSet = preparedStatement.executeQuery()) {
|
|
if (!resultSet.next()) {
|
|
return null;
|
|
}
|
|
return new GlobalSearchRow(
|
|
"vod",
|
|
resultSet.getString("stream_id"),
|
|
resultSet.getString("name"),
|
|
resultSet.getString("category_id"),
|
|
resultSet.getString("category_name"),
|
|
resultSet.getString("container_extension"),
|
|
"",
|
|
score
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private GlobalSearchRow findSeriesItemHit(Connection connection, String seriesId, double score) throws SQLException {
|
|
try (PreparedStatement preparedStatement = connection.prepareStatement("""
|
|
SELECT si.series_id, si.name, si.category_id, sc.category_name
|
|
FROM series_items si
|
|
LEFT JOIN series_categories sc ON sc.category_id = si.category_id
|
|
WHERE si.series_id = ?
|
|
""")) {
|
|
preparedStatement.setString(1, seriesId);
|
|
try (ResultSet resultSet = preparedStatement.executeQuery()) {
|
|
if (!resultSet.next()) {
|
|
return null;
|
|
}
|
|
return new GlobalSearchRow(
|
|
"series_item",
|
|
resultSet.getString("series_id"),
|
|
resultSet.getString("name"),
|
|
resultSet.getString("category_id"),
|
|
resultSet.getString("category_name"),
|
|
"",
|
|
"",
|
|
score
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private String normalizeType(String type) {
|
|
return type == null ? "" : type.trim().toLowerCase(Locale.ROOT);
|
|
}
|
|
|
|
private String safeLower(String value) {
|
|
return value == null ? "" : value.toLowerCase(Locale.ROOT);
|
|
}
|
|
|
|
private Connection openConnection() throws SQLException {
|
|
return DriverManager.getConnection(jdbcUrl);
|
|
}
|
|
|
|
private void inTransaction(SqlWork work) {
|
|
try (Connection connection = openConnection()) {
|
|
connection.setAutoCommit(false);
|
|
try {
|
|
work.execute(connection);
|
|
connection.commit();
|
|
} catch (Exception exception) {
|
|
connection.rollback();
|
|
throw exception;
|
|
} finally {
|
|
connection.setAutoCommit(true);
|
|
}
|
|
} catch (Exception exception) {
|
|
throw new IllegalStateException("Database transaction failed.", exception);
|
|
}
|
|
}
|
|
|
|
private String hashPassword(String password) {
|
|
try {
|
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
|
|
return Base64.getEncoder().encodeToString(hash);
|
|
} catch (NoSuchAlgorithmException e) {
|
|
throw new RuntimeException("SHA-256 not available", e);
|
|
}
|
|
}
|
|
|
|
// ===== DATA CLASSES =====
|
|
|
|
public static final class User {
|
|
private final int id;
|
|
private final String username;
|
|
private final String passwordHash;
|
|
private final Timestamp createdAt;
|
|
private final Timestamp updatedAt;
|
|
|
|
User(int id, String username, String passwordHash, Timestamp createdAt, Timestamp updatedAt) {
|
|
this.id = id;
|
|
this.username = username;
|
|
this.passwordHash = passwordHash;
|
|
this.createdAt = createdAt;
|
|
this.updatedAt = updatedAt;
|
|
}
|
|
|
|
public int getId() {
|
|
return id;
|
|
}
|
|
|
|
public String getUsername() {
|
|
return username;
|
|
}
|
|
|
|
public String getPasswordHash() {
|
|
return passwordHash;
|
|
}
|
|
|
|
public Timestamp getCreatedAt() {
|
|
return createdAt;
|
|
}
|
|
|
|
public Timestamp getUpdatedAt() {
|
|
return updatedAt;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "User{" +
|
|
"id=" + id +
|
|
", username='" + username + '\'' +
|
|
", createdAt=" + createdAt +
|
|
", updatedAt=" + updatedAt +
|
|
'}';
|
|
}
|
|
}
|
|
|
|
@FunctionalInterface
|
|
private interface SqlWork {
|
|
void execute(Connection connection) throws Exception;
|
|
}
|
|
|
|
record CategoryRow(String categoryId, String categoryName) {
|
|
}
|
|
|
|
record LiveStreamRow(String streamId, String name, String categoryId, String epgChannelId) {
|
|
}
|
|
|
|
record VodStreamRow(String streamId, String name, String categoryId, String containerExtension) {
|
|
}
|
|
|
|
record SeriesItemRow(String seriesId, String name, String categoryId) {
|
|
}
|
|
|
|
record SeriesEpisodeRow(String episodeId, String seriesId, String season, String episodeNum, String title,
|
|
String containerExtension) {
|
|
}
|
|
|
|
record FavoriteRow(String key, String mode, String id, String ext, String title, String categoryId, String seriesId,
|
|
String season, String episode, String url, long createdAt) {
|
|
}
|
|
|
|
record GlobalSearchRow(String kind, String id, String title, String categoryId, String categoryName,
|
|
String ext, String epgChannelId, double score) {
|
|
}
|
|
|
|
record LibraryCounts(int liveCategoryCount, int liveStreamCount, int vodCategoryCount, int vodStreamCount,
|
|
int seriesCategoryCount, int seriesItemCount, int seriesEpisodeCount) {
|
|
}
|
|
}
|