first commit

main
bluebamus 2025-10-31 15:56:19 +09:00
parent 78da3838f0
commit 09c86adf7b
486 changed files with 47300 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# VSCode 설정 - 기본적으로 모든 파일 제외
.vscode/*

3
backend-down.sh Normal file
View File

@ -0,0 +1,3 @@
#!/bin/bash
echo "🧹 Stopping backend, celery, and redis containers..."
docker compose -f docker-compose.backend.yml down

13
backend-up.sh Normal file
View File

@ -0,0 +1,13 @@
#!/bin/bash
set -e
echo "🧹 Stopping and cleaning up old backend containers..."
docker compose -f docker-compose.backend.yml -p backend down
echo "🔧 Building backend images..."
docker compose -f docker-compose.backend.yml -p backend build
echo "🚀 Starting backend stack (FastAPI + Celery Worker)..."
docker compose -f docker-compose.backend.yml -p backend up -d
echo "✅ Backend is up (Redis is expected to run on host:6379)"

22
backend/.coveragerc Normal file
View File

@ -0,0 +1,22 @@
[run]
source = app
omit =
*/tests/*
*/migrations/*
*/__init__.py
*/conftest.py
*/celery_app.py
*/workers/*
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
if TYPE_CHECKING:
@abstract
[html]
directory = htmlcov

69
backend/.env.prod Normal file
View File

@ -0,0 +1,69 @@
# PASSWORD_CRYPTO
CRYPTO_SECRET_KEY='UJMSDFliLZ9a6iPPKNgAuc_SAy4ISUmcwvbF0pS8XLs='
# DATABASE
DATABASE_USERNAME=postgres
DATABASE_PASSWORD='aio2o0656)^%^'
DATABASE_ADDRESS=host.docker.internal:5432
DATABASE_NAME=o2sound_v2
DATABASE_AUTO_COMMIT=false
DATABASE_AUTO_FLUSH=false
# REDIS
REDIS_HOST=host.docker.internal
REDIS_PORT=6379
REDIS_DB=0
# JWT
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_MINUTES=43200
JWT_SECRET_KEY=k0zH/43LgVhcJxCWWuiin4Y1rL4KT/KubHjqEPTsbLk
JWT_ALGORITHM=HS256
# KAKAO
KAKAO_REST_API_KEY=1b1aca55e745402a078c352cc4113f97
KAKAO_CLIENT_SECRET_KEY=l9dkPEEfvB1CNs1OgrgniBMeBKOKeJ5N
KAKAO_REDIRECT_URL=http://localhost:8000/social_auth/kakao/callback
# google
GOOGLE_CLIENT_ID=853325574184-c6bclkpmk4ja7pn99qjivclf4rdobost.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
GOOGLE_API_KEY=AIzaSyA7ULe-vQqCif6qTsiy7swgP5rRQBrCWhI
# SESSION
SESSION_SECRET_KEY=5c628fc0f9534c5ebd6f124ffb205ed868e520a948a3db8b74d5d2bc8c18db99
#OPENAI
OPENAI_API_KEY=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
# KTOPENAI
OPENAI_MIDM_BASE_URL="http://114.110.128.184:30345/v1"
OPENAI_MEDIM_API_KEY=dummy
OPENAI_MEDIM_MODEL="K-intelligence/Midm-2.0-Base-Instruct"
# Mureka
USEAPINET_API_TOKEN=user:1542-uKNtvnwqG9ZCkgeADlmnE
MUREAKA_SESSION_TOKEN=Ffgon6czYjg2EG9589qCfrLjJcLR2n6L
MUREAKA_USER_ID=58620752494593
# Mureka API ( fix )
MUSREKA_API_KEYS=op_mha4y17fL6SZztN4LYB1gsi1t3Fh6E7
# API
API_V1_STR='/api'
PROJECT_NAME='o2sound_v2 backend'
# Celery
CELERY_BROKER_URL=redis://localhost:6379/1
CELERY_RESULT_BACKEND=redis://localhost:6379/2
# RedisManager
RedisManager_URL="redis://host.docker.internal:6379"
# myBaseUrl
CURRENT_BACKEND_URL="https://o2sound-api.o2o.kr"
# AI Server
AI_SERVER_URL="https://proxy3.aitrain.ktcloud.com:10259/"
AI_SERVER_COOKIE_VALUE='appproxy_permit="YWRjMjczNjQ5OWRhMDc1MDU2MzQ1MTdmYmYxY2FkMjRlOTZmODFmMjdmODE0YThkOWUyODFjYzE0NTkwMmIzYw=="'

54
backend/.env.test Normal file
View File

@ -0,0 +1,54 @@
# Test Database Configuration
TEST_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/test_o2sound
# Test Environment - local로 설정
ENVIRONMENT=local
BE_ENV=local
# Redis (for testing)
REDIS_URL=redis://localhost:6379/1
# JWT Secret (for testing)
JWT_SECRET_KEY=test_secret_key_for_testing_only
JWT_ALGORITHM=HS256
JWT_EXPIRATION_MINUTES=60
# Application Settings (for testing)
PROJECT_NAME=O2Sound Test
VERSION=1.0.0
DEBUG=True
# Celery (for testing)
CELERY_BROKER_URL=redis://localhost:6379/1
CELERY_RESULT_BACKEND=redis://localhost:6379/1
# Google OAuth (for testing - use dummy values)
GOOGLE_CLIENT_ID=test_google_client_id
GOOGLE_CLIENT_SECRET=test_google_client_secret
GOOGLE_REDIRECT_URI=http://localhost:8000/api/v1/social/google/callback
# 테스트용 환경변수 (LocalConfig에 필요한 필드들)
CRYPTO_SECRET_KEY=test_crypto_secret_key
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=postgres
DATABASE_ADDRESS=localhost:5432
DATABASE_NAME=test_o2sound
DATABASE_AUTO_COMMIT=true
DATABASE_AUTO_FLUSH=true
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=1
KAKAO_REST_API_KEY=test_kakao_api_key
KAKAO_CLIENT_SECRET_KEY=test_kakao_secret
KAKAO_REDIRECT_URL=http://localhost:8000/kakao/callback
GOOGLE_CLIENT_ID=test_google_client_id
GOOGLE_CLIENT_SECRET=test_google_client_secret
GOOGLE_API_KEY=test_google_api_key
SESSION_SECRET_KEY=test_session_secret
OPENAI_API_KEY=test_openai_api_key
USEAPINET_API_TOKEN=test_useapi_token
MUREAKA_SESSION_TOKEN=test_mureaka_session
MUREAKA_USER_ID=test_mureaka_user
API_V1_STR=/api/v1
PROJECT_NAME=O2Sound Test
RedisManager_URL=redis://localhost:6379/1

16
backend/.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
.venv
__pycache__/
*.py[cod]
/uploads/*
!/uploads/.gitkeep
/crawling_results/*
!/crawling_results/.gitkeep
/success_log/*
!/success_log/.gitkeep
/ai/*
!/ai/.gitkeep
__pycache__/
*.pyc
*.pyo
*.local
.env.local

1
backend/.python-version Normal file
View File

@ -0,0 +1 @@
3.12.7

66
backend/Dockerfile Normal file
View File

@ -0,0 +1,66 @@
# Python 3.10 slim 이미지 사용 (pyproject.toml에서 ^3.10 지정됨)
FROM python:3.12-slim
# 작업 디렉토리 설정
WORKDIR /app
RUN mkdir -p /app/uploads
# 시스템 패키지 업데이트 및 필요한 패키지 설치
RUN apt-get update && apt-get install -y \
curl \
gnupg \
wget \
libgbm-dev \
libnss3 \
build-essential \
libjpeg-dev \
zlib1g-dev \
libpng-dev \
dos2unix \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Poetry 설치
RUN curl -sSL https://install.python-poetry.org | python3 - --version 1.8.3
# Google Chrome 설치 (최신 안정 버전)
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/googlechrome-linux-keyring.gpg \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/googlechrome-linux-keyring.gpg] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list \
&& apt-get update \
&& apt-get install -y google-chrome-stable \
&& rm -rf /var/lib/apt/lists/*
# Chrome 환경 변수 설정
ENV CHROME_BIN=/usr/bin/google-chrome
# Poetry 경로 환경 변수 설정
ENV PATH="/root/.local/bin:$PATH"
# Poetry 설정 - 가상환경 생성하지 않음 (컨테이너 내에서는 불필요)
RUN poetry config virtualenvs.create false
# pyproject.toml과 poetry.lock을 복사
COPY pyproject.toml poetry.lock* /app/
# 전역 패키지 설치 (로컬 가상환경이 아닌 글로벌 설치)
RUN poetry config virtualenvs.create false \
&& poetry install --no-root
# client_secret.json 파일이 있는지 확인하고 권한 설정
RUN if [ -f client_secret.json ]; then chmod 644 client_secret.json; fi
# __pycache__ 디렉토리 정리 (빌드 시 생성될 수 있음)
RUN find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
# 전체 앱 코드 복사
COPY . /app
# run.sh를 복사하고 이름 변경
COPY run-prod.sh /app/run-prod.sh
COPY run-celery.sh /app/run-celery.sh
# 줄바꿈 문자 변환 및 실행 권한 부여
RUN dos2unix /app/run-prod.sh /app/run-celery.sh \
&& chmod +x /app/run-prod.sh /app/run-celery.sh
# 컨테이너 시작 시 스크립트 실행
CMD ["/app/run-prod.sh"]

0
backend/README.md Normal file
View File

View File

@ -0,0 +1,22 @@
from celery import Celery
from app.core.env_setting import EnvSetting
settings = EnvSetting()
celery_app = Celery(
"worker",
broker=settings.CELERY_BROKER_URL,
backend=settings.CELERY_RESULT_BACKEND,
include=["app.workers.tasks"]
)
celery_app.conf.update(
task_serializer="json",
accept_content=["json"],
result_serializer="json",
timezone="UTC",
enable_utc=True,
task_track_started=True,
task_time_limit=30 * 60,
task_soft_time_limit=60,
)

View File

@ -0,0 +1,181 @@
from __future__ import annotations
from sqlalchemy import create_engine, Engine, text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from app.shared.logger import setup_logger
from typing import Generator
from sqlalchemy.orm import declarative_base
import abc
from app.core.env_setting import EnvSetting
logger = setup_logger(__name__)
Base = declarative_base()
settings = EnvSetting()
# def get_database_url() -> str:
# """PostgreSQL 데이터베이스 URL 생성"""
# return (
# f"postgresql://{settings.DATABASE_USERNAME}:"
# f"{settings.DATABASE_PASSWORD}@{settings.DATABASE_ADDRESS}/"
# f"{settings.DATABASE_NAME}"
# )
def create_engine_by_env():
db_url = get_database_url()
return create_engine(db_url, echo=True) # echo는 로그 확인용
def get_session_local():
engine = create_engine_by_env()
return sessionmaker(autocommit=False, autoflush=False, bind=engine)
# engine = create_engine(settings.DATABASE_URL)
# SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db() -> Generator[Session, None, None]:
SessionLocal = get_session_local()
db = SessionLocal()
logger.debug("DB session created")
try:
yield db
except Exception as e:
db.rollback()
logger.error(f"DB session error: {str(e)}")
raise
finally:
db.close()
logger.debug("DB session closed")
def get_database_url() -> str:
"""PostgreSQL 데이터베이스 URL 생성"""
return (
f"postgresql://{settings.DATABASE_USERNAME}:"
f"{settings.DATABASE_PASSWORD}@{settings.DATABASE_ADDRESS}/"
f"{settings.DATABASE_NAME}"
)
def create_database_engine():
"""데이터베이스 엔진 생성"""
database_url = get_database_url()
engine = create_engine(
database_url,
echo=True if settings.ENVIRONMENT == "local" else False, # 로컬에서만 SQL 로그 출력
pool_pre_ping=True, # 연결 상태 확인
pool_recycle=3600, # 1시간마다 연결 재생성
pool_size=10, # 연결 풀 크기
max_overflow=20 # 최대 추가 연결 수
)
return engine
# 엔진 인스턴스
engine = create_database_engine()
# 세션 팩토리 생성
SessionLocal = sessionmaker(
bind=engine,
autocommit=settings.DATABASE_AUTO_COMMIT,
autoflush=settings.DATABASE_AUTO_FLUSH,
expire_on_commit=False
)
# def get_db() -> Generator[Session, None, None]:
# """
# 데이터베이스 세션 의존성 주입 함수
# FastAPI Depends와 함께 사용
# """
# db = SessionLocal()
# try:
# yield db
# finally:
# db.close()
class DatabaseManager:
"""데이터베이스 관리자 클래스"""
@staticmethod
def get_session() -> Session:
"""새로운 데이터베이스 세션 반환"""
return SessionLocal()
@staticmethod
def close_session(db: Session) -> None:
"""데이터베이스 세션 종료"""
db.close()
@staticmethod
def commit_and_refresh(db: Session, instance) -> None:
"""커밋 후 인스턴스 새로고침"""
db.commit()
db.refresh(instance)
@staticmethod
def rollback(db: Session) -> None:
"""롤백 실행"""
db.rollback()
def create_session_maker(engine: Engine,settings):
return sessionmaker(autocommit=settings.DATABASE_AUTO_COMMIT,
autoflush=settings.DATABASE_AUTO_FLUSH,
bind=engine
)
def create_persistence_by_env():
engine = create_engine_by_env()
session_maker = create_session_maker(engine, settings)
return engine, session_maker
def create_tables():
try:
engine = create_engine_by_env()
Base.metadata.create_all(bind=engine)
logger.info("✅ All tables created successfully.")
except Exception as e:
logger.error(f"❌ Failed to create tables: {str(e)}")
raise
# # 🔥 FK 무시하고 안전하게 전체 테이블 삭제 (PostgreSQL 전용)
# def drop_tables():
# try:
# engine = create_engine_by_env()
# with engine.connect() as conn:
# conn.execute(text("SET session_replication_role = replica;"))
# BaseTable.metadata.drop_all(bind=engine)
# conn.execute(text("SET session_replication_role = DEFAULT;"))
# conn.commit()
# logger.warning("⚠️ All tables dropped successfully with FK constraints disabled temporarily.")
# except Exception as e:
# logger.error(f"❌ Failed to drop tables: {str(e)}")
# raise
# # =======
class AbstractUnitOfWork(abc.ABC):
def __enter__(self) -> AbstractUnitOfWork:
return self
def __exit__(self, *args):
self.rollback()
@abc.abstractmethod
def commit(self):
raise NotImplementedError
@abc.abstractmethod
def rollback(self):
raise NotImplementedError
@abc.abstractmethod
def flush(self):
raise NotImplementedError

View File

@ -0,0 +1,53 @@
from app.core.database.connection import Base, engine, SessionLocal
from app.core.database.session import get_db, DatabaseManager
from sqlalchemy import text
def create_tables():
'''모든 데이터베이스 테이블 생성'''
Base.metadata.create_all(bind=engine)
def drop_tables():
'''모든 데이터베이스 테이블 삭제 (개발용)'''
print("🔄 강제 테이블 삭제 시작...")
try:
with engine.connect() as conn:
# 모든 테이블 조회
result = conn.execute(text("""
SELECT tablename FROM pg_tables
WHERE schemaname = 'public'
"""))
tables = [row[0] for row in result]
print(f"발견된 테이블: {tables}")
# 각 테이블을 CASCADE로 삭제
for table in tables:
conn.execute(text(f"DROP TABLE IF EXISTS {table} CASCADE"))
print(f"{table} 테이블 삭제 완료")
conn.commit()
print("✅ 모든 테이블 삭제 완료")
except Exception as e:
print(f"❌ 테이블별 삭제 실패: {e}")
# 마지막 수단: 스키마 재생성
try:
with engine.connect() as conn:
conn.execute(text("DROP SCHEMA public CASCADE"))
conn.execute(text("CREATE SCHEMA public"))
conn.execute(text("GRANT ALL ON SCHEMA public TO postgres"))
conn.execute(text("GRANT ALL ON SCHEMA public TO public"))
conn.commit()
print("✅ 스키마 재생성 완료")
except Exception as final_error:
print(f"❌ 스키마 재생성도 실패: {final_error}")
# 패키지에서 내보낼 항목
__all__ = [
"Base",
"engine",
"SessionLocal",
"get_db",
"DatabaseManager",
"create_tables",
"drop_tables",
]

View File

@ -0,0 +1,40 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from app.core.env_setting import EnvSetting
Base = declarative_base()
settings = EnvSetting()
def get_database_url() -> str:
"""PostgreSQL 데이터베이스 URL 생성"""
return (
f"postgresql://{settings.DATABASE_USERNAME}:"
f"{settings.DATABASE_PASSWORD}@{settings.DATABASE_ADDRESS}/"
f"{settings.DATABASE_NAME}"
)
def create_database_engine():
"""데이터베이스 엔진 생성"""
database_url = get_database_url()
engine = create_engine(
database_url,
echo=True if settings.ENVIRONMENT == "local" else False, # 로컬에서만 SQL 로그 출력
pool_pre_ping=True, # 연결 상태 확인
pool_recycle=3600, # 1시간마다 연결 재생성
pool_size=10, # 연결 풀 크기
max_overflow=20 # 최대 추가 연결 수
)
return engine
# 엔진 인스턴스
engine = create_database_engine()
# 세션 팩토리 생성
SessionLocal = sessionmaker(
bind=engine,
autocommit=settings.DATABASE_AUTO_COMMIT,
autoflush=settings.DATABASE_AUTO_FLUSH,
expire_on_commit=False
)

View File

@ -0,0 +1,38 @@
from typing import Generator
from sqlalchemy.orm import Session
from app.core.database.connection import SessionLocal
def get_db() -> Generator[Session, None, None]:
"""
데이터베이스 세션 의존성 주입 함수
FastAPI Depends와 함께 사용
"""
db = SessionLocal()
try:
yield db
finally:
db.close()
class DatabaseManager:
"""데이터베이스 관리자 클래스"""
@staticmethod
def get_session() -> Session:
"""새로운 데이터베이스 세션 반환"""
return SessionLocal()
@staticmethod
def close_session(db: Session) -> None:
"""데이터베이스 세션 종료"""
db.close()
@staticmethod
def commit_and_refresh(db: Session, instance) -> None:
"""커밋 후 인스턴스 새로고침"""
db.commit()
db.refresh(instance)
@staticmethod
def rollback(db: Session) -> None:
"""롤백 실행"""
db.rollback()

View File

@ -0,0 +1,171 @@
import os
from typing import Literal
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from functools import lru_cache
class BaseConfig(BaseSettings):
'''기본 설정 클래스'''
ENVIRONMENT: Literal["local", "prod"] = "local"
# CRYPTO 설정
CRYPTO_SECRET_KEY: str = Field(..., description="CRYPTO 시크릿 키")
# DATABASE 설정
DATABASE_USERNAME: str = Field(..., description="데이터베이스 사용자 이름")
DATABASE_PASSWORD: str = Field(..., description="데이터베이스 비밀번호")
DATABASE_ADDRESS: str = Field(..., description="데이터베이스 주소")
DATABASE_NAME: str = Field(..., description="데이터베이스 이름")
DATABASE_AUTO_COMMIT: bool = Field(default=True, description="데이터베이스 자동 커밋 여부")
DATABASE_AUTO_FLUSH: bool = Field(default=True, description="데이터베이스 자동 플러시 여부")
# REDIS 설정
REDIS_HOST: str = Field(..., description="Redis 호스트")
REDIS_PORT: int = Field(..., description="Redis 포트")
REDIS_DB: int = Field(default=0, description="Redis 데이터베이스 번호")
# JWT 설정
ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=30, description="액세스 토큰 만료 시간(분)")
REFRESH_TOKEN_EXPIRE_MINUTES: int = Field(default=43200, description="리프레시 토큰 만료 시간(분)") # 30일
JWT_SECRET_KEY: str = Field(..., description="JWT 시크릿 키")
JWT_ALGORITHM: str = "HS256"
# KAKAO 설정
KAKAO_REST_API_KEY: str = Field(..., description="카카오 REST API 키")
KAKAO_CLIENT_SECRET_KEY: str = Field(..., description="카카오 클라이언트 시크릿 키")
KAKAO_REDIRECT_URL: str = Field(..., description="카카오 리다이렉트 URL")
# GOOGLE 설정
GOOGLE_CLIENT_ID: str = Field(..., description="구글 클라이언트 ID")
GOOGLE_CLIENT_SECRET: str = Field(..., description="구글 클라이언트 시크릿 키")
GOOGLE_API_KEY: str = Field(..., description="구글 API 키")
# SESSION
SESSION_SECRET_KEY: str = Field(..., description="유튜브 세션 시크릿 키")
# OpenAI 설정
OPENAI_API_KEY: str = Field(..., description="OpenAI API 키")
# KTOPENAI 설정
OPENAI_MIDM_BASE_URL: str = Field(..., description="KT OPENAI 베이스 URL")
OPENAI_MEDIM_API_KEY: str = Field(..., description="KT OPENAI API 키")
OPENAI_MEDIM_MODEL: str = Field(..., description="KT OPENAI 모델")
# Mureka 설정
USEAPINET_API_TOKEN: str = Field(..., description="USEAPI 토큰")
MUREAKA_SESSION_TOKEN: str = Field(..., description="Mureka 세션 아이디")
MUREAKA_USER_ID: str = Field(..., description="Mureka 유저 아이디")
# Mureka API ( fix )
MUSREKA_API_KEYS: str = Field(..., description="Mureka API 키")
# API
API_V1_STR: str = Field(..., description="API명")
PROJECT_NAME: str = Field(..., description="프로젝트 이름")
# Celery
CELERY_BROKER_URL: str = Field(..., description="CELERY BROKER URL")
CELERY_RESULT_BACKEND: str = Field(..., description="CERLERY RESULT BACK")
# RedisManager
RedisManager_URL: str = Field(..., description="RedisManager URL")
# myBaseUrl
CURRENT_BACKEND_URL: str = Field(..., description="현재 백엔드 URL")
# AI Server
AI_SERVER_URL: str = Field(..., description="AI Server URL")
AI_SERVER_COOKIE_VALUE: str = Field(..., description="AI Server Cookie Value")
@property
def is_production(self) -> bool:
return self.ENVIRONMENT == "prod"
@property
def is_local(self) -> bool:
return self.ENVIRONMENT == "local"
@property
def db_url(self) -> str:
return f"postgresql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_ADDRESS}/{self.DB_NAME}"
@property
def redis_url(self) -> str:
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
class LocalConfig(BaseConfig):
ENVIRONMENT: Literal["local", "prod"] = "local"
DB_ECHO: bool = True # SQL 쿼리 출력 O
model_config = SettingsConfigDict(
env_file=".env.local",
env_file_encoding="utf-8",
case_sensitive=True,
extra="ignore"
)
class ProdConfig(BaseConfig):
ENVIRONMENT: Literal["local", "prod"] = "prod"
DB_ECHO: bool = False # SQL 쿼리 출력 X
model_config = SettingsConfigDict(
env_file=".env.prod",
env_file_encoding="utf-8",
case_sensitive=True,
extra="ignore"
)
@lru_cache()
def get_config() -> BaseConfig:
'''환경에 따라 적절한 설정 반환 (캐싱됨)'''
env = os.getenv("BE_ENV", "local").lower()
if env == "prod":
return ProdConfig()
elif env == "local":
return LocalConfig()
else:
print(f"Warning: Unknown environment '{env}', using local config")
return LocalConfig()
def EnvSetting() -> BaseConfig:
return get_config()
# class LocalConfig(BaseConfig):
# '''로컬 설정 클래스'''
# model_config = SettingsConfigDict(
# env_file=".env.local",
# env_file_encoding="utf-8",
# case_sensitive=True,
# extra="ignore"
# )
# class ProdConfig(BaseConfig):
# '''프로덕션 설정 클래스'''
# model_config = SettingsConfigDict(
# env_file=".env.prod",
# env_file_encoding="utf-8",
# case_sensitive=True,
# extra="ignore"
# )
# def get_config() -> BaseConfig:
# '''환경에 따라 적절한 설정 반환 (캐싱됨)'''
# env = os.getenv("BE_ENV", "local").lower()
# if env == "prod":
# return ProdConfig()
# elif env == "local":
# return LocalConfig()
# else:
# # 기본값은 local
# print(f"Warning: Unknown environment '{env}', using local config")
# return LocalConfig()
# settings = get_config()

View File

@ -0,0 +1,49 @@
import json
from pathlib import Path
from authlib.integrations.starlette_client import OAuth
def get_oauth_client():
"""OAuth 클라이언트 생성 및 반환"""
oauth = OAuth()
try:
# client_secret.json 파일 로드
client_secret_path = Path("client_secret.json")
if not client_secret_path.exists():
raise FileNotFoundError("client_secret.json 파일을 찾을 수 없습니다.")
with open(client_secret_path, "r", encoding="utf-8") as f:
client_secrets = json.load(f)
# Google OAuth 클라이언트 정보 추출
if 'web' in client_secrets:
web_config = client_secrets['web']
elif 'installed' in client_secrets:
web_config = client_secrets['installed']
else:
raise ValueError("client_secret.json 파일 형식이 올바르지 않습니다.")
client_id = web_config.get('client_id')
client_secret = web_config.get('client_secret')
if not client_id or not client_secret:
raise ValueError("client_secret.json에서 client_id 또는 client_secret을 찾을 수 없습니다.")
# OAuth 클라이언트 등록
oauth.register(
name='google',
client_id=client_id,
client_secret=client_secret,
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
client_kwargs={
'scope': 'openid email profile'
}
)
print("✅ Google OAuth 클라이언트 설정 완료")
return oauth
except Exception as e:
print(f"❌ Google OAuth 설정 실패: {e}")
raise e

View File

@ -0,0 +1,34 @@
from .redis_manager import RedisManager
from .oauth_storage import GoogleOAuthStorage
# 전역 Redis 매니저 인스턴스
redis_manager = RedisManager()
google_storage = GoogleOAuthStorage(redis_manager)
async def get_redis_manager() -> RedisManager:
"""Redis 매니저 의존성"""
return redis_manager
async def get_google_storage() -> GoogleOAuthStorage:
"""Google 저장소 의존성"""
return google_storage
# 하위 호환성을 위한 별칭
async def get_oauth_storage() -> GoogleOAuthStorage:
"""OAuth 저장소 의존성 (하위 호환성)"""
return google_storage
async def get_youtube_storage() -> GoogleOAuthStorage:
"""YouTube 저장소 의존성 (하위 호환성)"""
return google_storage
__all__ = [
"RedisManager",
"GoogleOAuthStorage",
"redis_manager",
"google_storage",
"get_redis_manager",
"get_google_storage",
"get_oauth_storage", # 하위 호환성
"get_youtube_storage", # 하위 호환성
]

View File

@ -0,0 +1,97 @@
import json
from typing import Optional, Dict, Any
from .redis_manager import RedisManager
class GoogleOAuthStorage:
"""Google OAuth 전용 Redis 저장소"""
def __init__(self, redis_manager: RedisManager):
self.redis_manager = redis_manager
# ============== state 관리 ==============
async def store_oauth_state(self, state: str, state_data: Dict[str, Any], ttl: int = 300):
"""OAuth state 저장 (기본 5분 TTL)"""
try:
client = await self.redis_manager.get_client()
await client.set(
f"google_oauth_state:{state}",
json.dumps(state_data, ensure_ascii=False),
ex=ttl
)
print(f"✅ OAuth state 저장 완료: {state[:8]}...")
except Exception as e:
print(f"❌ OAuth state 저장 실패: {e}")
raise e
async def get_oauth_state(self, state: str) -> Optional[Dict[str, Any]]:
"""OAuth state 조회"""
try:
client = await self.redis_manager.get_client()
data = await client.get(f"google_oauth_state:{state}")
if data:
result = json.loads(data)
print(f"✅ OAuth state 조회 완료: {state[:8]}...")
return result
else:
print(f"⚠️ OAuth state 없음: {state[:8]}...")
return None
except Exception as e:
print(f"❌ OAuth state 조회 실패: {e}")
return None
async def delete_oauth_state(self, state: str):
"""OAuth state 삭제"""
try:
client = await self.redis_manager.get_client()
result = await client.delete(f"google_oauth_state:{state}")
if result:
print(f"✅ OAuth state 삭제 완료: {state[:8]}...")
else:
print(f"⚠️ 삭제할 OAuth state 없음: {state[:8]}...")
except Exception as e:
print(f"❌ OAuth state 삭제 실패: {e}")
# ================ token 관리 ==============
# front로 정보들을 넘겨주기 위한 token
async def store_temp_token(self, temp_token_id: str, token_data: Dict[str, Any], ttl: int = 300):
"""임시 토큰 저장 (기본 5분 TTL)"""
try:
client = await self.redis_manager.get_client()
await client.set(
f"google_temp_token:{temp_token_id}",
json.dumps(token_data, ensure_ascii=False),
ex=ttl
)
print(f"✅ 임시 토큰 저장 완료: {temp_token_id[:8]}...")
except Exception as e:
print(f"❌ 임시 토큰 저장 실패: {e}")
raise e
async def get_temp_token(self, temp_token_id: str) -> Optional[Dict[str, Any]]:
"""임시 토큰 조회"""
try:
client = await self.redis_manager.get_client()
data = await client.get(f"google_temp_token:{temp_token_id}")
if data:
result = json.loads(data)
print(f"✅ 임시 토큰 조회 완료: {temp_token_id[:8]}...")
return result
else:
print(f"⚠️ 임시 토큰 없음: {temp_token_id[:8]}...")
return None
except Exception as e:
print(f"❌ 임시 토큰 조회 실패: {e}")
return None
async def delete_temp_token(self, temp_token_id: str):
"""임시 토큰 삭제"""
try:
client = await self.redis_manager.get_client()
result = await client.delete(f"google_temp_token:{temp_token_id}")
if result:
print(f"✅ 임시 토큰 삭제 완료: {temp_token_id[:8]}...")
else:
print(f"⚠️ 삭제할 임시 토큰 없음: {temp_token_id[:8]}...")
except Exception as e:
print(f"❌ 임시 토큰 삭제 실패: {e}")

View File

@ -0,0 +1,12 @@
import redis.asyncio as redis
from app.core.env_setting import EnvSetting
settings = EnvSetting()
env = settings
redis_client = redis.Redis(
host=env.REDIS_HOST,
port=env.REDIS_PORT,
db=env.REDIS_DB,
decode_responses=True
)

View File

@ -0,0 +1,64 @@
import redis
import redis.asyncio as async_redis
from typing import Optional
from app.core.env_setting import EnvSetting
settings = EnvSetting()
class RedisManager:
def __init__(self, redis_url: Optional[str] = None):
self.redis_url = redis_url or settings.RedisManager_URL
self.async_client: Optional[async_redis.Redis] = None
self.sync_client: Optional[redis.Redis] = None
# 비동기 클라이언트 메서드들 (기존)
async def connect(self):
"""Redis 비동기 연결"""
if self.async_client is None:
self.async_client = async_redis.from_url(
self.redis_url,
encoding="utf-8",
decode_responses=True,
max_connections=20,
socket_connect_timeout=5,
socket_timeout=5,
)
return self.async_client
async def close(self):
"""Redis 비동기 연결 종료"""
if self.async_client:
await self.async_client.close()
self.async_client = None
async def get_client(self) -> async_redis.Redis:
"""Redis 비동기 클라이언트 반환"""
if self.async_client is None:
await self.connect()
return self.async_client
# 동기 클라이언트 메서드들 (새로 추가) ( celery 전용 )
def connect_sync(self):
"""Redis 동기 연결"""
if self.sync_client is None:
self.sync_client = redis.from_url(
self.redis_url,
encoding="utf-8",
decode_responses=True,
max_connections=20,
socket_connect_timeout=5,
socket_timeout=5,
)
return self.sync_client
def close_sync(self):
"""Redis 동기 연결 종료"""
if self.sync_client:
self.sync_client.close()
self.sync_client = None
def get_sync_client(self) -> redis.Redis:
"""Redis 동기 클라이언트 반환"""
if self.sync_client is None:
self.connect_sync()
return self.sync_client

View File

@ -0,0 +1,36 @@
from fastapi import Depends
from app.core.database import get_db
from sqlalchemy.orm import Session
from app.services.auth_service import AuthService
from app.services.social.google_service import GoogleService
from app.core.redis import redis_manager # 기존 인스턴스
from app.services.user_service import UserService
from app.services.order_service import OrderService
from app.services.video_service import VideoService
from app.services.get_my_video_result_service import GetMyVideoResultService
def get_auth_service(db: Session = Depends(get_db)) -> AuthService:
"""AuthService 의존성 주입"""
return AuthService(db)
def get_google_service() -> GoogleService:
"""Google OAuth 서비스 의존성 주입"""
return GoogleService(redis_manager)
def get_user_service(db: Session = Depends(get_db)) -> UserService:
"""UserService 의존성 주입"""
return UserService(db)
def get_order_service(db: Session = Depends(get_db)) -> OrderService:
"""OrderService 의존성 주입"""
return OrderService(db)
def get_video_service(db: Session = Depends(get_db)) -> VideoService:
"""VideoService 의존성 주입"""
return VideoService(db)
def get_get_my_video_result_service(db: Session = Depends(get_db)) -> GetMyVideoResultService:
'''GetMyVideoResultService 의존성 주입'''
return GetMyVideoResultService(db)

View File

@ -0,0 +1,66 @@
from enum import Enum
class MureakaGenres(str, Enum):
POP = "pop"
RNB = "r&b"
ROCK = "rock"
DISCO = "disco"
ELECTRONIC = "electronic"
FOLK = "folk"
FUNK = "funk"
COUNTRY = "country"
HIPHOP = "hip hop"
JAZZ = "jazz"
LATIN = "latin"
METAL = "metal"
BLUES = "blues"
PUNK = "punk"
REGGAE = "reggae"
SOUL = "soul"
INDIAN = "indian"
AFROBEAT = "afrobeat"
WORLD_MUSIC = "world music"
INDIE = "indie"
CLASSICAL = "classical"
EXPERIMENTAL = "experimental"
CHILDREN = "children"
DANCE = "dance"
SYNTHPOP_80S = "80s synthpop"
OLD_SCHOOL_RAP_89S = "89's old school rap"
DEATH_METAL = "death metal"
ALTERNATIVE_ROCK = "lternative rock"
JPOP = "J-pop"
EDM = "EDM"
GRITTY_MELODY = "gritty melody"
KPOP = "K-pop"
BALLAD = "ballad"
TIP_HOP = "tip-hop"
NEW_WAVE = "new wave"
ORCHESTRAL = "orchestral"
GAME_MUSIC = "game music"
SWING = "swing"
class MureakaMoods(str, Enum):
RELAXED = "relaxed"
ANGRY = "angry"
HAPPY = "happy"
ENERGETIC = "energetic"
SAD = "sad"
CALM = "calm"
INSPIRED = "inspired"
MYSTERIOUS = "mysterious"
MAJESTIC = "majestic"
QUIRKY = "quirky"
RESTLESS = "restless"
ROMANTIC = "romantic"
DARK = "dark"
WARM = "warm"
PASSIONATE = "passionate"
JOYFUL = "joyful"
EMOTIONAL = "emotional"
DARK_AMBIENT = "dark ambient"
EERIE = "eerie"
DREAMY = "dreamy"
MELANCHOLIC = "melancholic"
CHILL = "chill"
EMO = "emo"

View File

@ -0,0 +1,19 @@
from .user import User
from .item import Item
from .order import Order
from .video import Video
from .music import Music
from .photo import Photo
from .upload import Upload
from .channel import Channel
__all__ = [
"User",
"Item",
"Order",
"Video",
"Music",
"Photo",
"Upload",
"Channel",
]

View File

@ -0,0 +1,26 @@
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Boolean, func
from sqlalchemy.dialects.postgresql import UUID
from app.core.database import Base
class BaseModel(Base):
'''기본 모델 클래스'''
__abstract__ = True
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, unique=True, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
deleted = Column(Boolean, default=False, nullable=False)
def soft_delete(self):
'''소프트 삭제'''
self.deleted = True
self.updated_at = datetime.now(timezone.utc)
def to_dict(self) -> dict:
'''모델을 딕셔너리로 변환'''
return {
column.name: getattr(self, column.name)
for column in self.__table__.columns
}

View File

@ -0,0 +1,19 @@
from sqlalchemy import Column, String, ForeignKey
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
from sqlalchemy.dialects.postgresql import UUID
class Channel(BaseModel):
'''채널 모델'''
__tablename__ = "channels"
# 외래키 추가 (BaseModel의 id를 참조)
user_id = Column(UUID(as_uuid=True), ForeignKey('users.id'), nullable=False)
channel_id = Column(String, nullable=False, unique=True)
name = Column(String, nullable=False)
custom_url = Column(String, nullable=True)
platform = Column(String, nullable=False, default="youtube")
# 관계 설정 ( channels --- N:1 --- users )
user = relationship("User", back_populates="channels")

View File

@ -0,0 +1,26 @@
from sqlalchemy.orm import relationship
from sqlalchemy import Column, String, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from app.domain.models.base import BaseModel
from sqlalchemy.dialects.postgresql import ARRAY
class Item(BaseModel):
'''업체 모델'''
__tablename__ = "items"
# 외래키 - users 테이블의 id 참조
user_id = Column(UUID(as_uuid=True), ForeignKey('users.id'), nullable=False)
# 업체 정보
name = Column(String, nullable=False)
address = Column(String, nullable=False)
url = Column(String, nullable=False)
phone_number = Column(String, nullable=True)
thumbnail_url = Column(String, nullable=True)
hashtags = Column(ARRAY(String(30)), nullable=True)
description = Column(String, nullable=True)
# 관계 설정
# ( items --- N:1 --- users )
# ( items --- 1:N --- orders )
user = relationship("User", back_populates="items")
orders = relationship("Order", back_populates="item", cascade="all, delete-orphan")

View File

@ -0,0 +1,26 @@
from sqlalchemy import Column, String, ForeignKey, Boolean, Integer
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
class Music(BaseModel):
'''음악 모델'''
__tablename__ = "musics"
# 외래키
order_id = Column(UUID(as_uuid=True), ForeignKey('orders.id'), nullable=False)
index = Column(Integer, nullable=False) # 번호
is_selected = Column(Boolean, nullable=False) # 선택 여부
title = Column(String, nullable=False) # 제목
url = Column(String, nullable=False) # 주소
duration = Column(Integer, nullable=False) # 재생 시간
lyrics = Column(String, nullable=False) # 가사
# 관계 설정
# ( musics --- N:1 --- orders )
order = relationship("Order", back_populates="musics")
# ( musics --- 1:1 --- videos )
video = relationship("Video", back_populates="music", uselist=False, cascade="all, delete-orphan")

View File

@ -0,0 +1,27 @@
from sqlalchemy import Column, String, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID
from app.domain.models.base import BaseModel
class Order(BaseModel):
'''주문 모델'''
__tablename__ = "orders"
# 외래키들
user_id = Column(UUID(as_uuid=True), ForeignKey('users.id'), nullable=False)
item_id = Column(UUID(as_uuid=True), ForeignKey('items.id'), nullable=True)
# 주문 정보
status = Column(String, nullable=False)
# 관계 설정
# ( orders --- N:1 --- users )
# ( orders --- N:1 --- items )
user = relationship("User", back_populates="orders")
item = relationship("Item", back_populates="orders")
# ( orders --- 1:N --- videos )
# ( orders --- 1:N --- musics )
# ( orders --- 1:N --- photos )
videos = relationship("Video", back_populates="order", cascade="all, delete-orphan")
musics = relationship("Music", back_populates="order", cascade="all, delete-orphan")
photos = relationship("Photo", back_populates="order", cascade="all, delete-orphan")

View File

@ -0,0 +1,24 @@
from sqlalchemy import Column, String, ForeignKey, Boolean, Integer
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
class Photo(BaseModel):
'''사진 모델'''
__tablename__ = "photos"
# 외래키
order_id = Column(UUID(as_uuid=True), ForeignKey('orders.id'), nullable=False)
video_id = Column(UUID(as_uuid=True), ForeignKey('videos.id'), nullable=True)
# 사진 정보
name = Column(String, nullable=False) # 이름
url = Column(String, nullable=False) # 주소
video_index = Column(Integer, nullable=True) # 비디오 인덱스 ( 비디오에서의 순서 )
is_selected = Column(Boolean, nullable=False) # 선택 여부
# 관계 설정
# ( photos --- N:1 --- orders )
# ( photos --- N:1 --- videos )
order = relationship("Order", back_populates="photos")
video = relationship("Video", back_populates="photos")

View File

@ -0,0 +1,30 @@
from sqlalchemy import Column, String, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy import Column, String, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
from sqlalchemy.dialects.postgresql import ARRAY
class Upload(BaseModel):
'''업로드 모델'''
__tablename__ = "uploads"
# 외래키
video_id = Column(UUID(as_uuid=True), ForeignKey('videos.id'), nullable=False)
# 업로드 정보
title = Column(String, nullable=False) # 제목
description = Column(String, nullable=False) # 설명
tags = Column(ARRAY(String(30)), nullable=True) # 태그그
url = Column(String, nullable=False) # 주소
platform = Column(String, nullable=False) # 플랫폼
# 관계 설정
# ( uploads --- N:1 --- videos )
video = relationship("Video", back_populates="uploads")

View File

@ -0,0 +1,21 @@
from sqlalchemy import Column, String
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
class User(BaseModel):
'''사용자 모델'''
__tablename__ = "users"
user_id = Column(String, nullable=False, unique=True) # 로그인용 실제 아이디
name = Column(String, nullable=False)
password = Column(String, nullable=False)
# 추 후, 필요하면 column 수정 ( nullable 수정 )
email = Column(String, nullable=True, unique=True)
phone_number = Column(String, nullable=True, unique=True)
# 관계 설정 ( users --- 1:N --- items, orders, channels )
items = relationship("Item", back_populates="user", cascade="all, delete-orphan")
orders = relationship("Order", back_populates="user", cascade="all, delete-orphan")
channels = relationship("Channel", back_populates="user", cascade="all, delete-orphan")

View File

@ -0,0 +1,34 @@
from sqlalchemy import Column, String, ForeignKey, Boolean, Integer
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
class Video(BaseModel):
'''비디오 모델'''
__tablename__ = "videos"
# 외래키
music_id = Column(UUID(as_uuid=True), ForeignKey('musics.id'), nullable=False, unique=True)
order_id = Column(UUID(as_uuid=True), ForeignKey('orders.id'), nullable=False)
title = Column(String, nullable=False) # 비디오 제목
description = Column(String, nullable=False) # 비디오 설명
url = Column(String, nullable=False) # 비디오 주소
is_uploaded = Column(Boolean, nullable=False) # 비디오 업로드 여부
download_count = Column(Integer, nullable=False) # 비디오 다운로드 수
resolution = Column(String, nullable=False) # 비디오 해상도
status = Column(String, nullable=True, default="완료됨") # 비디오 상태 ( 예: 준비중, 업로드중, 업로드완료 )
thumbnail_url = Column(String, nullable=True) # 비디오 썸네일 주소
# 관계 설정
# ( videos --- 1:N --- uploads )
# ( videos --- 1:N --- photos )
# video가 없어도 photo는 있을 수 있어서 cascade 안함
uploads = relationship("Upload", back_populates="video", cascade="all, delete-orphan")
photos = relationship("Photo", back_populates="video", order_by="Photo.video_index")
# ( videos --- N:1 --- orders )
order = relationship("Order", back_populates="videos")
# ( videos --- 1:1 --- musics )
music = relationship("Music", back_populates="video", uselist=False)

View File

@ -0,0 +1,17 @@
import re
class Password(str):
def __new__(cls, value: str):
cls._validate(value)
return super().__new__(cls, value)
@classmethod
def _validate(cls, value: str):
if len(value) < 8:
raise ValueError("비밀번호는 8자 이상이어야 합니다.")
if not re.search(r"[A-Z]", value):
raise ValueError("비밀번호는 대문자 영어 1개 이상이어야 합니다.")
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', value):
raise ValueError("비밀번호에는 특수문자가 1개 이상 포함되어야 합니다.")

View File

@ -0,0 +1,10 @@
import re
class UserId(str):
def __init__(self, value: str):
if re.search(r"[^\w가-힣]", value):
raise ValueError("아이디에는 특수문자는 사용할 수 없습니다.")
self.value = value
def __str__(self):
return self.value

View File

@ -0,0 +1,10 @@
import re
class UserName(str):
def __init__(self, value: str):
if re.search(r"[^\w가-힣]", value):
raise ValueError("이름에는 특수문자는 사용할 수 없습니다.")
self.value = value
def __str__(self):
return self.value

View File

@ -0,0 +1,9 @@
Google Chrome
Copyright 2025 Google LLC. All rights reserved.
Chrome is made possible by the Chromium open source project
(https://www.chromium.org/) and other open source software
(chrome://credits).
See the Terms of Service at chrome://terms.

View File

@ -0,0 +1,8 @@
{
"name": "MEI Preload",
"icons": {},
"version": "1.0.7.1652906823",
"manifest_version": 2,
"update_url": "https://clients2.google.com/service/update2/crx",
"description": "Contains preloaded data for Media Engagement"
}

View File

@ -0,0 +1,6 @@
{
"manifest_version": 2,
"name": "Privacy Sandbox Attestations",
"version": "2025.5.21.0",
"pre_installed": true
}

View File

@ -0,0 +1,266 @@
https://2k.comhttps://33across.comhttps://360yield.comhttps://3lift.comhttps://ad-score.com https://ad.gthttps://adentifi.comhttps://adform.nethttps://adingo.jphttps://admatrix.jphttps://admixer.nethttps://adnami.iohttps://adnxs.comhttps://adsafeprotected.comhttps://adsrvr.orghttps://adthrive.comhttps://advividnetwork.comNhttps://aggregation-service-site-dot-clz200258-datateam-italy.ew.r.appspot.comhttps://anonymised.iohttps://aphub.aihttps://appier.nethttps://avads.nethttps://ayads.iohttps://bidswitch.nethttps://bidtheatre.nethttps://bing.comhttps://blendee.comhttps://bounceexchange.comhttps://bypass.jphttps://casalemedia.comhttps://cdn-net.comhttps://clickonometrics.plhttps://connected-stories.comhttps://crcldu.comhttps://creativecdn.comhttps://criteo.comhttps://ctnsnet.comhttps://daum.nethttps://display.iohttps://dotdashmeredith.comhttps://dotomi.comhttps://doubleclick.nethttps://dynalyst.jphttps://edkt.iohttps://ezoic.comhttps://fanbyte.comhttps://flashtalking.comhttps://fout.jphttps://fwmrm.nethttps://gama.globohttps://ghtinc.comhttps://gmossp-sp.jphttps://google-analytics.comhttps://gsspat.jphttps://gumgum.comhttps://html-load.comhttps://im-apps.nethttps://impact-ad.jphttps://indexww.comhttps://inmobi.comhttps://innovid.comhttps://jivox.comhttps://kidoz.nethttps://ladsp.comhttps://lucead.comhttps://mail.ruhttps://media.nethttps://mediaintelligence.dehttps://mediamath.comhttps://mediavine.comhttps://microad.jphttps://naver.comhttps://nhnace.comhttps://nodals.iohttps://onetag-sys.comhttps://openx.nethttps://optable.cohttps://outbrain.comhttps://pixfuture.com+https://privacy-sandbox-demos-ad-server.dev'https://privacy-sandbox-demos-dsp-a.dev'https://privacy-sandbox-demos-dsp-b.dev'https://privacy-sandbox-demos-dsp-x.dev'https://privacy-sandbox-demos-dsp-y.dev%https://privacy-sandbox-demos-dsp.dev*https://privacy-sandbox-demos-services.dev'https://privacy-sandbox-demos-ssp-a.dev'https://privacy-sandbox-demos-ssp-b.dev'https://privacy-sandbox-demos-ssp-x.dev'https://privacy-sandbox-demos-ssp-y.dev%https://privacy-sandbox-demos-ssp.dev https://privacy-sandbox-test.com0https://privacy-sandcastle-dev-ad-server.web.app-https://privacy-sandcastle-dev-dsp-a1.web.app-https://privacy-sandcastle-dev-dsp-b1.web.app,https://privacy-sandcastle-dev-dsp-x.web.app,https://privacy-sandcastle-dev-dsp-y.web.app*https://privacy-sandcastle-dev-dsp.web.app/https://privacy-sandcastle-dev-services.web.app,https://privacy-sandcastle-dev-ssp-a.web.app,https://privacy-sandcastle-dev-ssp-b.web.app,https://privacy-sandcastle-dev-ssp-x.web.app,https://privacy-sandcastle-dev-ssp-y.web.app*https://privacy-sandcastle-dev-ssp.web.apphttps://pub.networkhttps://pubmatic.comhttps://pubtm.comhttps://quantserve.comhttps://relevant-digital.comhttps://sascdn.comhttps://shinystat.comhttps://simeola.comhttps://singular.nethttps://sportradarserving.comhttps://t13.iohttps://teads.tvhttps://thepopradar.comhttps://theryn.iohttps://tncid.apphttps://toponad.comhttps://tpmark.nethttps://tribalfusion.comhttps://triptease.iohttps://uinterbox.comhttps://uol.com.br https://vg.nohttps://vpadn.comhttps://washingtonpost.comhttps://yahoo.co.jphttps://yahoo.comhttps://yandex.ruhttps://yelp.com
https://lwadm.com

https://finn.no

https://pinterest.com

https://r2b2.io

https://yieldmo.com

https://facebook.com

https://postrelease.com

https://elnacional.cat

https://dailymail.co.uk
!
https://dailymotion.com
%
https://audienceproject.com

https://cpx.to

https://worldhistory.org

https://usemax.de
!
https://audience360.com.au
#
https://youronlinechoices.eu

https://storygize.net

https://tailtarget.com
"
https://appsflyersdk.com

https://sephora.com

https://docomo.ne.jp

https://atomex.net

https://getcapi.co
%
https://wepowerconnections.com

https://aniview.com
#
https://adsmeasurement.com
%
https://creative-serving.com

https://adroll.com

https://trkkn.com

https://taboola.com

https://disqus.com

https://torneos.gg

https://globo.com

https://yieldlab.net

https://shinobi.jp
!
https://ebayadservices.com

https://acxiom.com

https://demand.supply

https://aqfer.com

https://i-mobile.co.jp

https://sitescout.com

https://tamedia.com.tw

https://doubleverify.com

https://dreammail.jp

https://cazamba.com

https://vidazoo.com

https://tiktok.com

https://iobeya.com
#
https://amazon-adsystem.com

https://primecaster.net

https://appsflyer.com

https://bluems.com
!
https://weborama-tech.ru
#
https://explorefledge.com

https://grxchange.gr

https://moshimo.com

https://coupang.com

https://momento.dev

https://unrulymedia.com

https://tangooserver.com

https://snapchat.com

https://stackadapt.com
"
https://kompaspublishing.nl

https://wp.pl

https://apex-football.com

https://get3rdspace.com
1
(https://paa-reporting-advertising.amazon

https://kargo.com

https://permutive.app

https://socdm.com
"
https://d-edgeconnect.media

https://atirun.com

https://insyta.com

https://validate.audio

https://tya-dev.com

https://ebis.ne.jp

https://a-mo.net

https://verve.com

https://onet.pl
(
https://smadexprivacysandbox.com

https://fandom.com

https://akpytela.cz
<
4https://shared-storage-demo-content-producer.web.app
"
https://media6degrees.com
6
/https://ptb-msmt-static-5jyy5ulagq-uc.a.run.app

https://logly.co.jp

https://nexxen.tech

https://xsoda.net
7
/https://shared-storage-demo-publisher-b.web.app

https://semafor.com

https://linkedin.com

https://samplicio.us

https://ad-stir.com
"
https://rocksolidrustic.com

https://retargetly.com
"
https://authorizedvault.com

https://gokwik.co

https://euleriancdn.net
&
https://adtrafficquality.google

https://undertone.com

https://connatix.com

https://beaconmax.com

https://weborama.fr

https://metro.co.uk

https://payment.goog

https://trip.com

https://eloan.co.jp

https://alketech.eu

https://appconsent.io
7
/https://shared-storage-demo-publisher-a.web.app
%
https://googlesyndication.com

https://seedtag.com

https://getyourguide.com

https://jkforum.net

https://superfine.org

https://adswizz.com

https://gunosy.com

https://admission.net

https://open-bid.com

https://presage.io

https://convertunits.com

https://azubiyo.de

https://deepintent.com

https://quora.com

https://elle.com
&
https://googleadservices.com
"
https://rubiconproject.com

https://boost-web.com

https://halcy.de

https://adscale.de

https://ingereck.net
"
https://bright-nurse.com


View File

@ -0,0 +1,8 @@
Google LLC and its affiliates ("Google") own all legal right, title and
interest in and to the content decryption module software ("Software") and
related documentation, including any intellectual property rights in the
Software. You may not use, modify, sell, or otherwise distribute the Software
without a separate license agreement with Google. The Software is not open
source software.
If you are interested in licensing the Software, please contact www.widevine.com

View File

@ -0,0 +1,33 @@
{
"manifest_version": 2,
"update_url": "https://clients2.google.com/service/update2/crx",
"name": "WidevineCdm",
"description": "Widevine Content Decryption Module",
"version": "4.10.2891.0",
"minimum_chrome_version": "68.0.3430.0",
"x-cdm-module-versions": "4",
"x-cdm-interface-versions": "10",
"x-cdm-host-versions": "10",
"x-cdm-codecs": "vp8,vp09,avc1,av01",
"x-cdm-persistent-license-support": false,
"x-cdm-supported-encryption-schemes": [
"cenc",
"cbcs"
],
"icons": {
"16": "imgs/icon-128x128.png",
"128": "imgs/icon-128x128.png"
},
"platforms": [
{
"os": "linux",
"arch": "x64",
"sub_package_path": "_platform_specific/linux_x64/"
},
{
"os": "linux",
"arch": "arm64",
"sub_package_path": "_platform_specific/linux_arm64/"
}
]
}

View File

@ -0,0 +1,163 @@
#!/bin/bash
# Copyright 2011 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# Running Chromium via this script makes it possible to set Chromium as the
# default browser directly out of a compile, without needing to package it.
DESKTOP="chromium-devel"
TITLE="Chromium"
usage() {
echo "$0 [--gdb] [--help] [--man-page] [--] [chrome-options]"
echo
echo " --gdb Start within gdb"
echo " --help This help screen"
echo " --man-page Open the man page in the tree"
}
# Check to see if there is a desktop file of the given name.
exists_desktop_file() {
# Build a search list from $XDG_DATA_HOME and $XDG_DATA_DIRS, the latter
# of which can itself be a colon-separated list of directories to search.
search="${XDG_DATA_HOME:-$HOME/.local/share}:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
IFS=:
for dir in $search; do
unset IFS
[ "$dir" -a -d "$dir/applications" ] || continue
[ -r "$dir/applications/$DESKTOP.desktop" ] && return
done
# Didn't find it in the search path.
return 1
}
# Checks a file to see if it's a 32 or 64-bit.
check_executable() {
out=$(file $(readlink -f $1) 2> /dev/null)
echo $out | grep -qs "ELF 32-bit LSB"
if [ $? = 0 ]; then
echo 32
return
fi
echo $out | grep -qs "ELF 64-bit LSB"
if [ $? = 0 ]; then
echo 64
return
fi
echo neither
}
# Generate a desktop file that will run this script.
generate_desktop_file() {
apps="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
mkdir -p "$apps"
cat > "$apps/$DESKTOP.desktop" << EOF
[Desktop Entry]
Version=1.0
Encoding=UTF-8
Name=$TITLE
Exec=$CHROME_WRAPPER %U
Terminal=false
Icon=$HERE/product_logo_48.png
Type=Application
Categories=Application;Network;WebBrowser;
MimeType=text/html;text/xml;application/xhtml_xml;
EOF
}
# Let the wrapped binary know that it has been run through the wrapper.
export CHROME_WRAPPER="`readlink -f "$0"`"
export CHROME_DESKTOP="$DESKTOP.desktop"
HERE="`dirname "$CHROME_WRAPPER"`"
# We include some xdg utilities next to the binary, and we want to prefer them
# over the system versions when we know the system versions are very old. We
# detect whether the system xdg utilities are sufficiently new to be likely to
# work for us by looking for xdg-settings. If we find it, we leave $PATH alone,
# so that the system xdg utilities (including any distro patches) will be used.
if ! which xdg-settings &> /dev/null; then
# Old xdg utilities. Prepend $HERE to $PATH to use ours instead.
export PATH="$HERE:$PATH"
else
# Use system xdg utilities. But first create mimeapps.list if it doesn't
# exist; some systems have bugs in xdg-mime that make it fail without it.
xdg_app_dir="${XDG_DATA_HOME:-$HOME/.local/share/applications}"
mkdir -p "$xdg_app_dir"
[ -f "$xdg_app_dir/mimeapps.list" ] || touch "$xdg_app_dir/mimeapps.list"
fi
# Always use our ffmpeg and other shared libs.
export LD_LIBRARY_PATH="$HERE:$HERE/lib:$HERE/lib.target${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
MISSING_LIBS=$(ldd "$HERE/chrome" 2> /dev/null |grep "not found$" | cut -d" " -f 1|sed 's/\t//')
CHROME_ARCH=$(check_executable "$HERE/chrome")
uname -m | grep -qs x86_64
if [ $? = 1 ]; then
LIBDIRS="/lib /lib32 /usr/lib /usr/lib32"
else
LIBDIRS="/lib64 /lib /usr/lib64 /usr/lib"
fi
echo $MISSING_LIBS | grep -qs libbz2.so.1.0
if [ $? = 0 ]; then
for dir in $LIBDIRS
do
if [ -e "$dir/libbz2.so.1" ]; then
LIB_ARCH=$(check_executable "$dir/libbz2.so.1")
if [ "$CHROME_ARCH" = "$LIB_ARCH" ]; then
ln -snf "$dir/libbz2.so.1" "$HERE/libbz2.so.1.0"
break;
fi
fi
done
fi
for lib in libnspr4.so.0d libnss3.so.1d libnssutil3.so.1d libplc4.so.0d libplds4.so.0d libsmime3.so.1d libssl3.so.1d
do
echo $MISSING_LIBS | grep -qs $lib
if [ $? = 0 ]; then
reallib=$(echo $lib | sed 's/\.[01]d$//')
for dir in $LIBDIRS
do
if [ -e "$dir/$reallib" ]; then
LIB_ARCH=$(check_executable "$dir/$reallib")
if [ "$CHROME_ARCH" = "$LIB_ARCH" ]; then
ln -snf "$dir/$reallib" "$HERE/$lib"
break;
fi
fi
done
fi
done
# Custom version string for this release. This can be used to add a downstream
# vendor string or release channel information.
export CHROME_VERSION_EXTRA="custom"
exists_desktop_file || generate_desktop_file
CMD_PREFIX=
ARGS=()
while [ "$#" -gt 0 ]; do
case "$1" in
"--")
shift
break ;;
"--gdb")
CMD_PREFIX="gdb --args" ;;
"--help")
usage
exit 0 ;;
"--man-page")
exec man "$HERE/../../chrome/app/resources/manpage.1.in" ;;
*)
ARGS=( "${ARGS[@]}" "$1" ) ;;
esac
shift
done
set -- "${ARGS[@]}" "$@"
exec $CMD_PREFIX "$HERE/chrome" "$@"

View File

@ -0,0 +1,30 @@
ca-certificates
fonts-liberation
libasound2 (>= 1.0.17)
libatk-bridge2.0-0 (>= 2.5.3)
libatk1.0-0 (>= 2.11.90)
libatspi2.0-0 (>= 2.9.90)
libc6 (>= 2.17)
libcairo2 (>= 1.6.0)
libcups2 (>= 1.6.0)
libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3
libdbus-1-3 (>= 1.9.14)
libexpat1 (>= 2.1~beta3)
libgbm1 (>= 17.1.0~rc2)
libglib2.0-0 (>= 2.39.4)
libgtk-3-0 (>= 3.9.10) | libgtk-4-1
libnspr4 (>= 2:4.9-2~)
libnss3 (>= 2:3.35)
libpango-1.0-0 (>= 1.14.0)
libudev1 (>= 183)
libvulkan1
libx11-6 (>= 2:1.4.99.1)
libxcb1 (>= 1.9.2)
libxcomposite1 (>= 1:0.4.4-1)
libxdamage1 (>= 1:1.1)
libxext6
libxfixes3
libxkbcommon0 (>= 0.5.0)
libxrandr2
wget
xdg-utils (>= 1.0.2)

Some files were not shown because too many files have changed in this diff Show More