Changed project name to be IPTV Manager Service
All checks were successful
AWS Deploy on Push / build (push) Successful in 8m29s

This commit is contained in:
2025-05-29 16:09:52 -05:00
parent e25f8c1ecd
commit eaab1ef998
22 changed files with 202 additions and 138 deletions

View File

@@ -4,7 +4,7 @@ MOCK_AUTH=true/false
DB_USER=MyDBUser
DB_PASSWORD=MyDBPassword
DB_HOST=MyDBHost
DB_NAME=iptv_updater
DB_NAME=iptv_manager
FREEDNS_User=MyFreeDNSUsername
FREEDNS_Password=MyFreeDNSPassword

View File

@@ -58,7 +58,7 @@ jobs:
run: |
INSTANCE_IDS=$(aws ec2 describe-instances \
--region us-east-2 \
--filters "Name=tag:Name,Values=IptvUpdaterStack/IptvUpdaterInstance" \
--filters "Name=tag:Name,Values=IptvManagerStack/IptvManagerInstance" \
"Name=instance-state-name,Values=running" \
--query "Reservations[].Instances[].InstanceId" \
--output text)
@@ -69,11 +69,11 @@ jobs:
--instance-ids "$INSTANCE_ID" \
--document-name "AWS-RunShellScript" \
--parameters 'commands=[
"cd /home/ec2-user/iptv-updater-aws",
"cd /home/ec2-user/iptv-manager-service",
"git pull",
"pip3 install -r requirements.txt",
"alembic upgrade head",
"sudo systemctl restart iptv-updater"
"sudo systemctl restart iptv-manager"
]'
done

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.2
rev: v0.11.12
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]

View File

@@ -1,4 +1,6 @@
{
"python.terminal.activateEnvironment": true,
"python.terminal.activateEnvInCurrentTerminal": true,
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff",
"ruff.importStrategy": "fromEnvironment",
@@ -31,10 +33,13 @@
"cluflogo",
"clulogo",
"cpulogo",
"crond",
"cronie",
"cuflgo",
"CUNF",
"cunflogo",
"cuulogo",
"datname",
"deadstreams",
"delenv",
"delogo",
@@ -50,6 +55,7 @@
"freedns",
"fullchain",
"gitea",
"httpx",
"iptv",
"isort",
"KHTML",
@@ -61,7 +67,9 @@
"ondelete",
"onupdate",
"passlib",
"PGPASSWORD",
"poolclass",
"psql",
"psycopg",
"pycache",
"pycodestyle",
@@ -82,6 +90,7 @@
"testdb",
"testpass",
"testpaths",
"testuser",
"uflogo",
"umlogo",
"usefixtures",

View File

@@ -1,6 +1,6 @@
# IPTV Updater AWS
# IPTV Manager Service
An automated IPTV playlist and EPG updater service deployed on AWS infrastructure using CDK.
An automated IPTV playlist and EPG manager service deployed on AWS infrastructure using CDK.
## Overview
@@ -25,7 +25,7 @@ This project provides a service for automatically updating IPTV playlists and El
```bash
git clone <repo-url>
cd iptv-updater-aws
cd iptv-manager-service
```
2. Copy the example environment file:
@@ -144,13 +144,13 @@ scripts/ # Utility scripts for deployment and management
The following environment variables are required:
| Variable | Description |
|----------|-------------|
| FREEDNS_User | FreeDNS username |
| FREEDNS_Password | FreeDNS password |
| DOMAIN_NAME | Your domain name |
| SSH_PUBLIC_KEY | SSH public key for EC2 access |
| REPO_URL | Repository URL |
| Variable | Description |
| ----------------- | ------------------------------------ |
| FREEDNS_User | FreeDNS username |
| FREEDNS_Password | FreeDNS password |
| DOMAIN_NAME | Your domain name |
| SSH_PUBLIC_KEY | SSH public key for EC2 access |
| REPO_URL | Repository URL |
| LETSENCRYPT_EMAIL | Email for Let's Encrypt certificates |
## Security Notes

View File

@@ -15,7 +15,8 @@ config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# Setup target metadata for autogenerate support
# add your model's MetaData object here
# for 'autogenerate' support
target_metadata = Base.metadata
# Override sqlalchemy.url with dynamic credentials

View File

@@ -1,59 +0,0 @@
"""Add priority and in_use fields
Revision ID: 036879e47172
Revises:
Create Date: 2025-05-26 19:21:32.285656
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '036879e47172'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
# 1. Create priorities table if not exists
if not op.get_bind().engine.dialect.has_table(op.get_bind(), 'priorities'):
op.create_table('priorities',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('description', sa.String(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
# 2. Insert default priorities (skip if already exists)
op.execute("""
INSERT INTO priorities (id, description)
VALUES (100, 'High'), (200, 'Medium'), (300, 'Low')
ON CONFLICT (id) DO NOTHING
""")
# Add new columns with temporary nullable=True
op.add_column('channels_urls', sa.Column('in_use', sa.Boolean(), nullable=True))
op.add_column('channels_urls', sa.Column('priority_id', sa.Integer(), nullable=True))
# Set default values
op.execute("UPDATE channels_urls SET in_use = false, priority_id = 100")
# Convert to NOT NULL
op.alter_column('channels_urls', 'in_use', nullable=False)
op.alter_column('channels_urls', 'priority_id', nullable=False)
op.create_foreign_key(None, 'channels_urls', 'priorities', ['priority_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('channels_urls_priority_id_fkey', 'channels_urls', type_='foreignkey')
op.drop_column('channels_urls', 'priority_id')
op.drop_column('channels_urls', 'in_use')
op.drop_table('priorities')
# ### end Alembic commands ###

View File

@@ -0,0 +1,79 @@
"""create initial tables
Revision ID: 95b61a92455a
Revises:
Create Date: 2025-05-29 14:42:16.239587
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '95b61a92455a'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('channels',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tvg_id', sa.String(), nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('group_title', sa.String(), nullable=False),
sa.Column('tvg_name', sa.String(), nullable=True),
sa.Column('tvg_logo', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('group_title', 'name', name='uix_group_title_name')
)
op.create_table('priorities',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('description', sa.String(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('channels_urls',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('channel_id', sa.UUID(), nullable=False),
sa.Column('url', sa.String(), nullable=False),
sa.Column('in_use', sa.Boolean(), nullable=False),
sa.Column('priority_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['channel_id'], ['channels.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['priority_id'], ['priorities.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
# Seed initial priorities
op.bulk_insert(
sa.Table(
'priorities',
sa.MetaData(),
sa.Column('id', sa.Integer),
sa.Column('description', sa.String),
),
[
{'id': 100, 'description': 'High'},
{'id': 200, 'description': 'Medium'},
{'id': 300, 'description': 'Low'},
]
)
def downgrade() -> None:
"""Downgrade schema."""
# Remove seeded priorities
op.execute("DELETE FROM priorities WHERE id IN (100, 200, 300);")
# Drop tables
op.drop_table('channels_urls')
op.drop_table('priorities')
op.drop_table('channels')

6
app.py
View File

@@ -3,7 +3,7 @@ import os
import aws_cdk as cdk
from infrastructure.stack import IptvUpdaterStack
from infrastructure.stack import IptvManagerStack
app = cdk.App()
@@ -31,9 +31,9 @@ if missing_vars:
f"Missing required environment variables: {', '.join(missing_vars)}"
)
IptvUpdaterStack(
IptvManagerStack(
app,
"IptvUpdaterStack",
"IptvManagerStack",
freedns_user=freedns_user,
freedns_password=freedns_password,
domain_name=domain_name,

View File

@@ -15,8 +15,8 @@ async def lifespan(app: FastAPI):
app = FastAPI(
lifespan=lifespan,
title="IPTV Updater API",
description="API for IPTV Updater service",
title="IPTV Manager API",
description="API for IPTV Manager service",
version="1.0.0",
)
@@ -60,7 +60,7 @@ app.openapi = custom_openapi
@app.get("/")
async def root():
return {"message": "IPTV Updater API"}
return {"message": "IPTV Manager API"}
# Include routers

View File

@@ -19,16 +19,16 @@ def get_db_credentials():
ssm = boto3.client("ssm", region_name=AWS_REGION)
try:
host = ssm.get_parameter(Name="/iptv-updater/DB_HOST", WithDecryption=True)[
host = ssm.get_parameter(Name="/iptv-manager/DB_HOST", WithDecryption=True)[
"Parameter"
]["Value"]
user = ssm.get_parameter(Name="/iptv-updater/DB_USER", WithDecryption=True)[
user = ssm.get_parameter(Name="/iptv-manager/DB_USER", WithDecryption=True)[
"Parameter"
]["Value"]
password = ssm.get_parameter(
Name="/iptv-updater/DB_PASSWORD", WithDecryption=True
Name="/iptv-manager/DB_PASSWORD", WithDecryption=True
)["Parameter"]["Value"]
dbname = ssm.get_parameter(Name="/iptv-updater/DB_NAME", WithDecryption=True)[
dbname = ssm.get_parameter(Name="/iptv-manager/DB_NAME", WithDecryption=True)[
"Parameter"
]["Value"]
return f"postgresql://{user}:{password}@{host}/{dbname}"

View File

@@ -3,10 +3,11 @@ version: '3.8'
services:
postgres:
image: postgres:13
container_name: postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: iptv_updater
POSTGRES_DB: iptv_manager
ports:
- "5432:5432"
volumes:

View File

@@ -6,7 +6,7 @@ services:
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: iptv_updater
POSTGRES_DB: iptv_manager
ports:
- "5432:5432"
volumes:
@@ -20,7 +20,7 @@ services:
DB_USER: postgres
DB_PASSWORD: postgres
DB_HOST: postgres
DB_NAME: iptv_updater
DB_NAME: iptv_manager
MOCK_AUTH: "true"
ports:
- "8000:8000"

View File

@@ -9,7 +9,7 @@ from aws_cdk import aws_ssm as ssm
from constructs import Construct
class IptvUpdaterStack(Stack):
class IptvManagerStack(Stack):
def __init__(
self,
scope: Construct,
@@ -27,7 +27,7 @@ class IptvUpdaterStack(Stack):
# Create VPC
vpc = ec2.Vpc(
self,
"IptvUpdaterVPC",
"IptvManagerVPC",
max_azs=2, # Need at least 2 AZs for RDS subnet group
nat_gateways=0, # No NAT Gateway to stay in free tier
subnet_configuration=[
@@ -44,7 +44,7 @@ class IptvUpdaterStack(Stack):
# Security Group
security_group = ec2.SecurityGroup(
self, "IptvUpdaterSG", vpc=vpc, allow_all_outbound=True
self, "IptvManagerSG", vpc=vpc, allow_all_outbound=True
)
security_group.add_ingress_rule(
@@ -66,18 +66,18 @@ class IptvUpdaterStack(Stack):
"Allow PostgreSQL traffic for tunneling",
)
# Key pair for IPTV Updater instance
# Key pair for IPTV Manager instance
key_pair = ec2.KeyPair(
self,
"IptvUpdaterKeyPair",
key_pair_name="iptv-updater-key",
"IptvManagerKeyPair",
key_pair_name="iptv-manager-key",
public_key_material=ssh_public_key,
)
# Create IAM role for EC2
role = iam.Role(
self,
"IptvUpdaterRole",
"IptvManagerRole",
assumed_by=iam.ServicePrincipal("ec2.amazonaws.com"),
)
@@ -114,7 +114,7 @@ class IptvUpdaterStack(Stack):
# EC2 Instance
instance = ec2.Instance(
self,
"IptvUpdaterInstance",
"IptvManagerInstance",
vpc=vpc,
vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC),
instance_type=ec2.InstanceType.of(
@@ -132,7 +132,7 @@ class IptvUpdaterStack(Stack):
# Option: 2: Create Elastic IP (not free tier compatible)
# eip = ec2.CfnEIP(
# self, "IptvUpdaterEIP",
# self, "IptvManagerEIP",
# domain="vpc",
# instance_id=instance.instance_id
# )
@@ -140,8 +140,8 @@ class IptvUpdaterStack(Stack):
# Add Cognito User Pool
user_pool = cognito.UserPool(
self,
"IptvUpdaterUserPool",
user_pool_name="iptv-updater-users",
"IptvManagerUserPool",
user_pool_name="iptv-manager-users",
self_sign_up_enabled=False, # Only admins can create users
password_policy=cognito.PasswordPolicy(
min_length=8,
@@ -156,7 +156,7 @@ class IptvUpdaterStack(Stack):
# Add App Client with the correct callback URL
client = user_pool.add_client(
"IptvUpdaterClient",
"IptvManagerClient",
access_token_validity=Duration.minutes(60),
id_token_validity=Duration.minutes(60),
refresh_token_validity=Duration.days(1),
@@ -171,8 +171,8 @@ class IptvUpdaterStack(Stack):
# Add domain for hosted UI
domain = user_pool.add_domain(
"IptvUpdaterDomain",
cognito_domain=cognito.CognitoDomainOptions(domain_prefix="iptv-updater"),
"IptvManagerDomain",
cognito_domain=cognito.CognitoDomainOptions(domain_prefix="iptv-manager"),
)
# Read the userdata script with proper path resolution
@@ -226,7 +226,7 @@ class IptvUpdaterStack(Stack):
# Create RDS PostgreSQL instance (free tier compatible - db.t3.micro)
db = rds.DatabaseInstance(
self,
"IptvUpdaterDB",
"IptvManagerDB",
engine=rds.DatabaseInstanceEngine.postgres(
version=rds.PostgresEngineVersion.VER_13
),
@@ -240,7 +240,7 @@ class IptvUpdaterStack(Stack):
security_groups=[rds_sg],
allocated_storage=10,
max_allocated_storage=10,
database_name="iptv_updater",
database_name="iptv_manager",
removal_policy=RemovalPolicy.DESTROY,
deletion_protection=False,
publicly_accessible=False, # Avoid public IPv4 charges
@@ -255,25 +255,25 @@ class IptvUpdaterStack(Stack):
ssm.StringParameter(
self,
"DBHostParam",
parameter_name="/iptv-updater/DB_HOST",
parameter_name="/iptv-manager/DB_HOST",
string_value=db.db_instance_endpoint_address,
)
ssm.StringParameter(
self,
"DBNameParam",
parameter_name="/iptv-updater/DB_NAME",
string_value="iptv_updater",
parameter_name="/iptv-manager/DB_NAME",
string_value="iptv_manager",
)
ssm.StringParameter(
self,
"DBUserParam",
parameter_name="/iptv-updater/DB_USER",
parameter_name="/iptv-manager/DB_USER",
string_value=db.secret.secret_value_from_json("username").to_string(),
)
ssm.StringParameter(
self,
"DBPassParam",
parameter_name="/iptv-updater/DB_PASSWORD",
parameter_name="/iptv-manager/DB_PASSWORD",
string_value=db.secret.secret_value_from_json("password").to_string(),
)

View File

@@ -2,7 +2,7 @@
# Update system and install required packages
dnf update -y
dnf install -y python3-pip git cronie nginx certbot python3-certbot-nginx
dnf install -y python3-pip git cronie nginx certbot python3-certbot-nginx postgresql awscli
# Start and enable crond service
systemctl start crond
@@ -11,27 +11,48 @@ systemctl enable crond
cd /home/ec2-user
git clone ${REPO_URL}
cd iptv-updater-aws
cd iptv-manager-service
# Install Python packages with --ignore-installed to prevent conflicts with RPM packages
pip3 install --ignore-installed -r requirements.txt
# Retrieve DB credentials from SSM Parameter Store
export DB_HOST=$(aws ssm get-parameter --name "/iptv-manager/DB_HOST" --query "Parameter.Value" --output text)
export DB_NAME=$(aws ssm get-parameter --name "/iptv-manager/DB_NAME" --query "Parameter.Value" --output text)
export DB_USER=$(aws ssm get-parameter --name "/iptv-manager/DB_USER" --query "Parameter.Value" --output text)
export DB_PASSWORD=$(aws ssm get-parameter --name "/iptv-manager/DB_PASSWORD" --query "Parameter.Value" --output text)
# Set PGPASSWORD for psql to use
export PGPASSWORD=$DB_PASSWORD
# Wait for PostgreSQL to be ready
echo "Waiting for PostgreSQL to start..."
until psql -h $DB_HOST -U $DB_USER -d postgres -c '\q'; do
sleep 1
done
echo "PostgreSQL is ready."
# Create database if it does not exist
DB_EXISTS=$(psql -h $DB_HOST -U $DB_USER -d postgres -tc "SELECT 1 FROM pg_database WHERE datname = '$DB_NAME';")
if [ -z "$DB_EXISTS" ]; then
echo "Creating database $DB_NAME..."
psql -h $DB_HOST -U $DB_USER -d postgres -c "CREATE DATABASE $DB_NAME;"
echo "Database $DB_NAME created."
fi
# Run database migrations
alembic upgrade head
# Seed initial priorities
python3 -c "from app.utils.database import SessionLocal; from app.models.db import Priority; db = SessionLocal(); db.add_all([Priority(id=100, description='High'), Priority(id=200, description='Medium'), Priority(id=300, description='Low')]); db.commit()"
# Create systemd service file
cat << 'EOF' > /etc/systemd/system/iptv-updater.service
cat << 'EOF' > /etc/systemd/system/iptv-manager.service
[Unit]
Description=IPTV Updater Service
Description=IPTV Manager Service
After=network.target
[Service]
Type=simple
User=ec2-user
WorkingDirectory=/home/ec2-user/iptv-updater-aws
WorkingDirectory=/home/ec2-user/iptv-manager-service
ExecStart=/usr/local/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000
EnvironmentFile=/etc/environment
Restart=always
@@ -56,7 +77,7 @@ sudo mkdir -p /etc/nginx/ssl
--reloadcmd "service nginx force-reload"
# Create nginx config
cat << EOF > /etc/nginx/conf.d/iptvUpdater.conf
cat << EOF > /etc/nginx/conf.d/iptvManager.conf
server {
listen 80;
server_name ${DOMAIN_NAME} *.${DOMAIN_NAME};
@@ -83,5 +104,5 @@ EOF
# Start nginx service
systemctl enable nginx
systemctl start nginx
systemctl enable iptv-updater
systemctl start iptv-updater
systemctl enable iptv-manager
systemctl start iptv-manager

View File

@@ -5,12 +5,21 @@ python_functions = test_*
asyncio_mode = auto
filterwarnings =
ignore::DeprecationWarning:botocore.auth
ignore:The 'app' shortcut is now deprecated:DeprecationWarning:httpx._client
# Coverage configuration
addopts =
--cov=app
--cov-report=term-missing
# Test environment variables
env =
MOCK_AUTH=true
DB_USER=test_user
DB_PASSWORD=test_password
DB_HOST=localhost
DB_NAME=iptv_manager_test
# Test markers
markers =
slow: mark tests as slow running

View File

@@ -14,4 +14,8 @@ psycopg2-binary==2.9.9
alembic==1.16.1
pytest==8.1.1
pytest-asyncio==0.23.6
pytest-mock==3.12.0
pytest-mock==3.12.0
pytest-cov==4.1.0
pytest-env==1.1.1
httpx==0.27.0
pre-commit

View File

@@ -25,7 +25,7 @@ cdk deploy --app="python3 ${PWD}/app.py"
# Update application on running instances
INSTANCE_IDS=$(aws ec2 describe-instances \
--region us-east-2 \
--filters "Name=tag:Name,Values=IptvUpdaterStack/IptvUpdaterInstance" \
--filters "Name=tag:Name,Values=IptvManagerStack/IptvManagerInstance" \
"Name=instance-state-name,Values=running" \
--query "Reservations[].Instances[].InstanceId" \
--output text)
@@ -35,7 +35,7 @@ for INSTANCE_ID in $INSTANCE_IDS; do
aws ssm send-command \
--instance-ids "$INSTANCE_ID" \
--document-name "AWS-RunShellScript" \
--parameters '{"commands":["cd /home/ec2-user/iptv-updater-aws && git pull && pip3 install -r requirements.txt && alembic upgrade head && sudo systemctl restart iptv-updater"]}' \
--parameters '{"commands":["cd /home/ec2-user/iptv-manager-service && git pull && pip3 install -r requirements.txt && alembic upgrade head && sudo systemctl restart iptv-manager"]}' \
--no-cli-pager \
--no-paginate
done

View File

@@ -10,10 +10,4 @@ pre-commit install-hooks
pre-commit autoupdate
# Verify pytest setup
python3 -m pytest
# Initialize and run database migrations
alembic upgrade head
# Seed initial data
python3 -c "from app.utils.database import SessionLocal; from app.models.db import Priority; db = SessionLocal(); db.add_all([Priority(id=100, description='High'), Priority(id=200, description='Medium'), Priority(id=300, description='Low')]); db.commit()"
python3 -m pytest

View File

@@ -1,21 +1,26 @@
#!/bin/bash
set -e
# Start PostgreSQL
docker-compose -f docker/docker-compose-db.yml up -d
# Set mock auth and database environment variables
# Set environment variables
export MOCK_AUTH=true
export DB_HOST=localhost
export DB_USER=postgres
export DB_PASSWORD=postgres
export DB_HOST=localhost
export DB_NAME=iptv_updater
export DB_NAME=iptv_manager
echo "Ensuring database $DB_NAME exists using conditional DDL..."
PGPASSWORD=$DB_PASSWORD docker exec -i postgres psql -U $DB_USER <<< "SELECT 'CREATE DATABASE $DB_NAME' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$DB_NAME')\gexec"
echo "Database $DB_NAME check complete."
# Run database migrations
alembic upgrade head
# Start FastAPI
nohup uvicorn app.main:app --host 127.0.0.1 --port 8000 > app.log 2>&1 &
echo $! > iptv-updater.pid
echo $! > iptv-manager.pid
echo "Services started:"
echo "- PostgreSQL running on localhost:5432"

View File

@@ -1,9 +1,9 @@
#!/bin/bash
# Stop FastAPI
if [ -f iptv-updater.pid ]; then
kill $(cat iptv-updater.pid)
rm iptv-updater.pid
if [ -f iptv-manager.pid ]; then
kill $(cat iptv-manager.pid)
rm iptv-manager.pid
echo "Stopped FastAPI"
fi

View File

@@ -16,7 +16,7 @@ def test_root_endpoint(client):
"""Test root endpoint returns expected message"""
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "IPTV Updater API"}
assert response.json() == {"message": "IPTV Manager API"}
def test_openapi_schema_generation(client):