first commit
parent
78da3838f0
commit
09c86adf7b
|
|
@ -0,0 +1,2 @@
|
|||
# VSCode 설정 - 기본적으로 모든 파일 제외
|
||||
.vscode/*
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
echo "🧹 Stopping backend, celery, and redis containers..."
|
||||
docker compose -f docker-compose.backend.yml down
|
||||
|
|
@ -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)"
|
||||
|
|
@ -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
|
||||
|
|
@ -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=="'
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
3.12.7
|
||||
Binary file not shown.
|
|
@ -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,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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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", # 하위 호환성
|
||||
]
|
||||
|
|
@ -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}")
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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"
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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개 이상 포함되어야 합니다.")
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
}
|
||||
Binary file not shown.
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Privacy Sandbox Attestations",
|
||||
"version": "2025.5.21.0",
|
||||
"pre_installed": true
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
Binary file not shown.
|
|
@ -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/"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
|
@ -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" "$@"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue