initial commit for GitHub

This commit is contained in:
Lucas Jensen
2024-12-01 19:15:25 -08:00
commit 925b334e4c
91 changed files with 8031 additions and 0 deletions

1
server/app/__init__.py Normal file
View File

@@ -0,0 +1 @@
from app.main import app

View File

@@ -0,0 +1,11 @@
# sql table names
ARTISTS_TABLE = "artists"
ALBUMS_TABLE = "albums"
ARTICLES_TABLE = "articles"
ARTWORK_TABLE = "artwork"
ART_MEDIUM_TABLE = "medium"
SOCIAL_TABLE = "social"
BIO_CONTENT_TABLE = "bio_content"
QUOTES_TABLE = "quotes"
VIDEOS_TABLE = "videos"
SERVICES_TABLE = "services"

View File

@@ -0,0 +1,3 @@
from .controller import Controller
controller = Controller()

View File

@@ -0,0 +1,62 @@
from pathlib import Path
from fastapi import status
from fastapi.exceptions import HTTPException
from git import Repo
from icecream import ic
from app.model import Album, Artwork, Bio, ProfessionalService, Quote, SocialUrl, Video
class Controller:
def __init__(self) -> None:
pass
async def get_version(self) -> str:
repo = Repo(Path.cwd().parent.joinpath(".git"))
tags = [tag.tag for tag in repo.tags if tag.tag is not None]
tags.sort(key=lambda t: t.tagged_date)
return tags[-1].tag
async def get_all_videos(self) -> list[Video]:
return Video.select_all()
def get_one_video(self, video_id: int) -> Video:
if (video := Video.select_one(video_id)) is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return video
async def get_all_quotes(self) -> list[Quote]:
return Quote.select_all()
def get_one_quote(self, quote_id: int) -> Quote:
if (quote := Quote.select_one(quote_id)) is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return quote
async def get_all_albums(self) -> list[Album]:
return Album.select_all()
def get_one_album(self, album_id: int) -> Album:
if (album := Album.select_one(album_id)) is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return album
async def get_all_artwork(self) -> list[Artwork]:
return Artwork.select_all()
def get_one_artwork(self, artwork_id: int) -> Artwork:
if (artwork := Artwork.select_one(artwork_id)) is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return artwork
async def get_bio(self) -> Bio:
return Bio.select_one()
async def get_all_professional_services(self) -> list[ProfessionalService]:
return ProfessionalService.select_all()
def get_one_professional_service(self, service_id) -> ProfessionalService:
if (service := ProfessionalService.select_one(service_id)) is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return service

64
server/app/db/DDL.sql Normal file
View File

@@ -0,0 +1,64 @@
-- Auto generated by DBeaver
-- meganjohns.articles definition
CREATE TABLE `articles` (
`article_id` int(11) NOT NULL AUTO_INCREMENT,
`article_title` varchar(255) NOT NULL,
`body` varchar(255) NOT NULL,
`video_url` varchar(255) DEFAULT NULL,
`is_featured` tinyint(1) DEFAULT 0,
PRIMARY KEY (`article_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- meganjohns.artists definition
CREATE TABLE `artists` (
`artist_id` int(11) NOT NULL AUTO_INCREMENT,
`artist_name` varchar(255) NOT NULL,
`artist_url` varchar(255) NOT NULL,
PRIMARY KEY (`artist_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- meganjohns.medium definition
CREATE TABLE `medium` (
`medium_id` int(11) NOT NULL AUTO_INCREMENT,
`medium_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`medium_id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- meganjohns.albums definition
CREATE TABLE `albums` (
`album_id` int(11) NOT NULL AUTO_INCREMENT,
`album_name` varchar(255) NOT NULL,
`year` int(11) NOT NULL,
`artist_id` int(11) NOT NULL,
`spotify_url` varchar(255) DEFAULT NULL,
`itunes_url` varchar(255) DEFAULT NULL,
`bandcamp_url` varchar(255) DEFAULT NULL,
`apple_music_url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`album_id`),
KEY `artist_id` (`artist_id`),
CONSTRAINT `albums_ibfk_1` FOREIGN KEY (`artist_id`) REFERENCES `artists` (`artist_id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- meganjohns.artwork definition
CREATE TABLE `artwork` (
`artwork_id` int(11) NOT NULL AUTO_INCREMENT,
`medium_id` int(11) NOT NULL,
`artwork_name` varchar(255) NOT NULL,
`source_url` varchar(255) NOT NULL,
`year` int(11) NOT NULL,
`size` varchar(255) DEFAULT NULL,
PRIMARY KEY (`artwork_id`),
UNIQUE KEY `artwork_name` (`artwork_name`),
KEY `medium_id` (`medium_id`),
CONSTRAINT `artwork_ibfk_1` FOREIGN KEY (`medium_id`) REFERENCES `medium` (`medium_id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

View File

37
server/app/db/conn.py Normal file
View File

@@ -0,0 +1,37 @@
import os
import mysql.connector
from dotenv import load_dotenv
class DBException(Exception):
pass
def connect_db() -> mysql.connector.MySQLConnection:
"""
Connects to the MySQL database using credentials from the .env file.
Returns a MySQLConnection object which can be used by the database query layer.
Credential values are validated and an exception is raised if any are missing.
"""
load_dotenv()
host = os.getenv("DB_HOST")
user = os.getenv("DB_USER")
password = os.getenv("DB_PASSWORD")
database = os.getenv("DB_DATABASE")
if None in [host, user, password, database]:
raise DBException("Missing database credentials")
try:
return mysql.connector.connect(
host=host,
user=user,
password=password,
database=database,
auth_plugin="mysql_native_password",
) # type: ignore
except mysql.connector.Error as err:
raise DBException("Could not connect to database") from err

72
server/app/main.py Normal file
View File

@@ -0,0 +1,72 @@
from asyncio import gather
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from app.controller import controller
from app.model.albums import Album
from app.model.artwork import Artwork
from app.model.bio import Bio, ProfessionalService
from app.model.quotes import Quote
from app.model.video import Video
from app.routers.albums import router as albums_router
from app.routers.artwork import router as artwork_router
from app.routers.bio import router as bio_router
from app.routers.quotes import router as quotes_router
from app.routers.videos import router as videos_router
from .origins import origins
app = FastAPI()
app.include_router(albums_router)
app.include_router(artwork_router)
app.include_router(bio_router)
app.include_router(quotes_router)
app.include_router(videos_router)
# noinspection PyTypeChecker
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class MeganJohns(BaseModel):
albums: list[Album]
artwork: list[Artwork]
quotes: list[Quote]
videos: list[Video]
bio: Bio
professional_services: list[ProfessionalService]
version: str
@app.get("/")
async def root() -> MeganJohns:
albums, artwork, bio, quotes, videos, services = await gather(
controller.get_all_albums(),
controller.get_all_artwork(),
controller.get_bio(),
controller.get_all_quotes(),
controller.get_all_videos(),
controller.get_all_professional_services(),
)
return MeganJohns(
albums=albums,
artwork=artwork,
quotes=quotes,
videos=videos,
bio=bio,
professional_services=services,
version=await controller.get_version(),
)
@app.get("/version")
async def version() -> str:
return await controller.get_version()

View File

@@ -0,0 +1,5 @@
from .albums import Album, Artist
from .artwork import Artwork, Medium
from .bio import Bio, ProfessionalService, SocialUrl
from .quotes import Quote
from .video import Video

View File

@@ -0,0 +1,94 @@
from typing import Optional, Type
from icecream import ic
from pydantic import HttpUrl
from app.constants import ALBUMS_TABLE, ARTISTS_TABLE
from app.model.model_object import ModelObject
from app.model.response_object import ResponseObject
class Artist(ModelObject, ResponseObject):
artist_name: str
artist_url: Optional[HttpUrl] = None
class Album(ModelObject, ResponseObject):
album_name: str
release_year: int
artist: Artist
front_artwork_url: HttpUrl
spotify_url: Optional[HttpUrl] = None
itunes_url: Optional[HttpUrl] = None
bandcamp_url: Optional[HttpUrl] = None
apple_music_url: Optional[HttpUrl] = None
rear_artwork_url: Optional[HttpUrl] = None
bandcamp_player: Optional[str] = None
@classmethod
def select_one(
cls, album_id: int, table_name: str = ALBUMS_TABLE
) -> "Album | None":
cursor, conn = cls._get_cursor_and_conn()
cursor.execute(
f"""-- sql
SELECT
al.id,
al.album_name ,
al.release_year,
al.artist_id ,
al.spotify_url ,
al.itunes_url ,
al.bandcamp_url ,
al.apple_music_url ,
al.front_artwork_url ,
al.rear_artwork_url ,
al.bandcamp_player ,
ar.artist_name ,
ar.artist_url
FROM {table_name} al
LEFT JOIN {ARTISTS_TABLE} ar
ON al.artist_id = ar.id
WHERE al.id = {album_id};
"""
)
data: dict = cursor.fetchone() # type: ignore
cls._close_cursor_and_conn(cursor, conn)
return cls._construct(cls, data) if data else None
@classmethod
def select_all(cls, table_name: str = ALBUMS_TABLE) -> list["Album"]:
cursor, conn = cls._get_cursor_and_conn()
cursor.execute(
f"""-- sql
SELECT
al.id,
al.album_name ,
al.release_year,
al.artist_id ,
al.spotify_url ,
al.itunes_url ,
al.bandcamp_url ,
al.apple_music_url ,
al.front_artwork_url ,
al.rear_artwork_url ,
al.bandcamp_player ,
ar.artist_name ,
ar.artist_url
FROM {table_name} al
LEFT JOIN {ARTISTS_TABLE} ar
ON al.artist_id = ar.id;
"""
)
data: dict = cursor.fetchall() # type: ignore
cls._close_cursor_and_conn(cursor, conn)
return sorted(
[cls._construct(cls, row) for row in data],
key=lambda album: album.release_year,
reverse=True,
)
@classmethod
def _construct(cls, Obj: Type, data: dict) -> "Album":
data["artist"] = Artist(**data)
return Obj(**data)

View File

@@ -0,0 +1,11 @@
from typing import Optional
from pydantic import BaseModel, HttpUrl
class Article(BaseModel):
id: int
article_title: str
body: str
is_featured: Optional[bool] = False
video_url: Optional[HttpUrl] = None

View File

@@ -0,0 +1,85 @@
from typing import Optional, Type
from pydantic import HttpUrl
from app.constants import ART_MEDIUM_TABLE, ARTWORK_TABLE
from app.model.model_object import ModelObject
from app.model.response_object import ResponseObject
class Medium(ModelObject, ResponseObject):
medium_name: str
class Artwork(ModelObject, ResponseObject):
artwork_name: str
source: HttpUrl
thumbnail: HttpUrl
is_featured: bool = False
medium: Optional[Medium] = None
release_year: Optional[int] = None
size: Optional[str] = None
@classmethod
def select_one(
cls, artwork_id: int, table_name: str = ARTWORK_TABLE
) -> "Artwork | None":
cursor, conn = cls._get_cursor_and_conn()
cursor.execute(
f"""-- sql
SELECT
a.id,
a.medium_id ,
a.artwork_name ,
a.source ,
a.thumbnail ,
a.is_featured ,
a.release_year ,
a.`size` ,
m.id as medium_id,
m.medium_name
FROM {table_name} a
LEFT JOIN {ART_MEDIUM_TABLE} m ON a.medium_id = m.id
WHERE a.id = {artwork_id}
"""
)
row: dict = cursor.fetchone() # type: ignore
cls._close_cursor_and_conn(cursor, conn)
return cls._construct(cls, row) if row else None
@classmethod
def select_all(cls, table_name: str = ARTWORK_TABLE) -> list["Artwork"]:
cursor, conn = cls._get_cursor_and_conn()
cursor.execute(
f"""-- sql
SELECT
a.id,
a.medium_id ,
a.artwork_name ,
a.source ,
a.thumbnail ,
a.is_featured ,
a.release_year ,
a.`size` ,
m.id as medium_id,
m.medium_name
FROM {table_name} a
LEFT JOIN {ART_MEDIUM_TABLE} m ON a.medium_id = m.id
"""
)
rows: list[dict] = cursor.fetchall() # type: ignore
cls._close_cursor_and_conn(cursor, conn)
return sorted(
[cls._construct(cls, row) for row in rows],
key=lambda a: (a.is_featured, a.release_year if a.release_year else 0),
reverse=True,
)
@classmethod
def _construct(cls, Obj: Type, row: dict) -> "Artwork":
row["medium"] = (
Medium(id=row["medium_id"], medium_name=row["medium_name"])
if row.get("medium_id")
else None
)
return Obj(**row)

62
server/app/model/bio.py Normal file
View File

@@ -0,0 +1,62 @@
from pydantic import HttpUrl
from app.constants import BIO_CONTENT_TABLE, SERVICES_TABLE, SOCIAL_TABLE
from app.model.model_object import ModelObject
from app.model.response_object import ResponseObject
class ProfessionalService(ModelObject, ResponseObject):
service_name: str
@classmethod
def select_all(
cls, table_name: str = SERVICES_TABLE
) -> list["ProfessionalService"]:
return [
cls._construct(ProfessionalService, row)
for row in super().select_all(table_name)
]
@classmethod
def select_one(
cls, obj_id: int, table_name: str = SERVICES_TABLE
) -> "ProfessionalService | None":
return cls._construct(
ProfessionalService, super().select_one(obj_id, table_name)
)
class SocialUrl(ModelObject, ResponseObject):
social_name: str
social_url: HttpUrl
@classmethod
def select_one(
cls, obj_id: int, table_name: str = SOCIAL_TABLE
) -> "SocialUrl | None":
return cls._construct(SocialUrl, super().select_one(obj_id, table_name))
@classmethod
def select_all(cls, table_name: str = SOCIAL_TABLE) -> list["SocialUrl"]:
return [
cls._construct(SocialUrl, row) for row in super().select_all(table_name)
]
class Bio(ModelObject, ResponseObject):
name: str
bio: str
social_urls: list[SocialUrl]
@classmethod
def select_one(cls, table_name: str = BIO_CONTENT_TABLE) -> "Bio":
bio_data = super().select_one(1, table_name)
bio_content = bio_data.get("content", "") if bio_data else ""
socials = SocialUrl.select_all()
bio = Bio(bio=bio_content, social_urls=socials, name="Megan Johns")
del bio.id
return bio
@classmethod
def select_all(cls, **args) -> None:
raise NotImplemented

View File

@@ -0,0 +1,59 @@
from typing import Any, Optional, Type
from icecream import ic
from mysql.connector.connection import MySQLConnection
from mysql.connector.cursor import MySQLCursor
from pydantic import BaseModel
from app.db.conn import connect_db
class ModelObject:
@classmethod
def select_one(cls, obj_id: int, table_name: str = "") -> dict | None:
if not table_name:
raise Exception(
"table_name cannot be an empty string. Check default arguments."
)
cursor, conn = cls._get_cursor_and_conn()
cursor.execute(
f"""-- sql
SELECT *
FROM {table_name}
WHERE id = {obj_id};
"""
)
data: dict[Any, Any] | None = cursor.fetchone() # type: ignore
cls._close_cursor_and_conn(cursor, conn)
return data
@classmethod
def select_all(cls, table_name: str = "") -> list[dict]:
if not table_name:
raise Exception(
"table_name cannot be an empty string. Check default arguments."
)
cursor, conn = cls._get_cursor_and_conn()
cursor.execute(
f"""-- sql
SELECT *
FROM {table_name};
"""
)
data: list[dict] = cursor.fetchall() # type: ignore
cls._close_cursor_and_conn(cursor, conn)
return data
@classmethod
def _get_cursor_and_conn(cls) -> tuple[MySQLCursor, MySQLConnection]:
conn = connect_db()
return conn.cursor(dictionary=True), conn
@classmethod
def _close_cursor_and_conn(cls, cursor: MySQLCursor, conn: MySQLConnection) -> None:
cursor.close()
conn.close()
@classmethod
def _construct(cls, Obj: Type, data: dict | None) -> Any:
return Obj(**data) if data is not None else None

View File

@@ -0,0 +1,22 @@
from typing import Optional, Type
from icecream import ic
from pydantic import HttpUrl
from app.constants import QUOTES_TABLE
from app.model.model_object import ModelObject
from app.model.response_object import ResponseObject
class Quote(ModelObject, ResponseObject):
body: str
author: str
source: Optional[HttpUrl] = None
@classmethod
def select_one(cls, obj_id: int, table_name: str = QUOTES_TABLE) -> "Quote | None":
return cls._construct(cls, super().select_one(obj_id, table_name))
@classmethod
def select_all(cls, table_name: str = QUOTES_TABLE) -> list["Quote"]:
return [cls._construct(cls, row) for row in super().select_all(table_name)]

View File

@@ -0,0 +1,7 @@
from typing import Optional
from pydantic import BaseModel
class ResponseObject(BaseModel):
id: Optional[int] = None

25
server/app/model/video.py Normal file
View File

@@ -0,0 +1,25 @@
from typing import Optional, Type
from pydantic import HttpUrl
from app.constants import VIDEOS_TABLE
from app.model.model_object import ModelObject
from app.model.response_object import ResponseObject
class Video(ModelObject, ResponseObject):
title: str
subtitle: str
description: str # html
source: HttpUrl
embedded_player_iframe: str # an iframe from YouTube/Vimeo
website: Optional[HttpUrl] = None
@classmethod
def select_one(cls, obj_id: int, table_name: str = VIDEOS_TABLE) -> "Video | None":
data = super().select_one(obj_id, table_name)
return cls._construct(cls, data)
@classmethod
def select_all(cls, table_name: str = VIDEOS_TABLE) -> list["Video"]:
return [cls._construct(cls, row) for row in super().select_all(table_name)]

8
server/app/origins.py Normal file
View File

@@ -0,0 +1,8 @@
origins = [
"http://localhost:5173",
"http://127.0.0.1:5173/",
"https://mj.lucasjensen.me",
"https://meganjohns.com/",
"https://www.meganjohns.com",
"https://www.meganjohns.com/",
]

View File

View File

@@ -0,0 +1,20 @@
from fastapi import APIRouter
from app.controller import controller
from app.model import Album
router = APIRouter(
prefix="/albums",
tags=["albums"],
responses={404: {"description": "Not found"}},
)
@router.get("/")
async def all_albums() -> list[Album]:
return await controller.get_all_albums()
@router.get("/{album_id}")
async def album(album_id: int) -> Album:
return controller.get_one_album(album_id)

View File

@@ -0,0 +1,20 @@
from fastapi import APIRouter
from app.controller import controller
from app.model import Artwork
router = APIRouter(
prefix="/artwork",
tags=["artwork"],
responses={404: {"description": "Not found"}},
)
@router.get("/")
async def all_artwork() -> list[Artwork]:
return await controller.get_all_artwork()
@router.get("/{artwork_id}")
async def artwork(artwork_id: int) -> Artwork:
return controller.get_one_artwork(artwork_id)

20
server/app/routers/bio.py Normal file
View File

@@ -0,0 +1,20 @@
from fastapi import APIRouter
from app.controller import controller
from app.model.bio import Bio, ProfessionalService
router = APIRouter(
prefix="/bio",
tags=["bio"],
responses={404: {"description": "Not found"}},
)
@router.get("/")
async def bio() -> Bio:
return await controller.get_bio()
@router.get("/services")
async def services() -> list[ProfessionalService]:
return await controller.get_all_professional_services()

View File

@@ -0,0 +1,20 @@
from fastapi import APIRouter
from app.controller import controller
from app.model import Quote
router = APIRouter(
prefix="/quotes",
tags=["quotes"],
responses={404: {"description": "Not found"}},
)
@router.get("/")
async def all_quotes() -> list[Quote]:
return await controller.get_all_quotes()
@router.get("/{id}")
async def quote(id: int) -> Quote:
return controller.get_one_quote(id)

View File

@@ -0,0 +1,20 @@
from fastapi import APIRouter
from app.controller import controller
from app.model import Video
router = APIRouter(
prefix="/videos",
tags=["videos"],
responses={404: {"description": "Not found"}},
)
@router.get("/")
async def all_videos() -> list[Video]:
return await controller.get_all_videos()
@router.get("/{video_id}")
async def video(video_id: int) -> Video:
return controller.get_one_video(video_id)

View File

View File

@@ -0,0 +1,4 @@
<p>American indie rock singer-songwriter, <strong>Megan Johns</strong>, originates from a small urban oasis in endless Illinois fields. At age 16, she recorded her first original folk pop album to local acclaim, written secretly in early adolescence.</p>
<p>Johns has performed hundreds of live original shows, including worldwide, supporting Johanna Warren, R. Ring (Kelley Deal, The Breeders), Jenny Owen Youngs, Swooning (Briana Marela), Zion I Crew (with Swords & The Struggle), Liz Cooper (& The Stampede), Lola Kirk, Said The Whale, Angie Heaton, King Washington, and many more.</p>
<p>She has musically collaborated with Zoot Suit Riot and Tori Amos Double-Platinum engineer, Billy Barnett (Gung Ho Studio), Allison Kraus, Hum, Ani DiFranco, and Ludacris engineer, Mark Rubel (Pogo Studio/Blackbird), Adam Schmitt, Mountain Goats bassist, Peter Hughes, Andy Lund, many loved ones, accomplished string and horn players, emcees, and bands.</p>
<p>Based in the Pacific Northwest for the last decade, Johns' mission is to share creativity. She now records her own music and alternatively performs as MoonWish.</p>

43
server/app/scripts/bump.py Executable file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
from pathlib import Path
from subprocess import run
UTF8 = "utf-8"
def bump():
tags = [int(tag) for tag in find_tags() if tag.isnumeric()]
tag_name = max(tags) + 1
cmds = [f"git tag {tag_name}", f"git push origin tag {tag_name}"]
for cmd in cmds:
cmd = cmd.split(" ")
output = run(cmd, capture_output=True, encoding=UTF8)
if output.stdout:
print(output.stdout)
if output.stderr:
print(output.stderr)
def find_tags() -> list[str]:
dot_git = find_git_dir()
tags_dir = dot_git / "refs" / "tags"
for _, _, files in tags_dir.walk():
return files
raise Exception("Error parsing tags")
def find_git_dir() -> Path:
dot_git = ".git"
def _find_git_dir(cwd: Path = Path(__file__)) -> Path:
for _, dirs, _ in cwd.walk(top_down=False):
if dot_git in dirs:
return cwd / dot_git
return _find_git_dir(cwd.parent)
return _find_git_dir()
if __name__ == "__main__":
bump()

12
server/app/scripts/run.py Normal file
View File

@@ -0,0 +1,12 @@
import subprocess
from pathlib import Path
def main() -> None:
print("starting app in development mode")
curr_dir = Path(__file__).resolve().parent.absolute()
script = curr_dir / "run.sh"
try:
subprocess.run(["sh", script], check=True)
except KeyboardInterrupt:
return

3
server/app/scripts/run.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
uvicorn app:app --reload --port 8000

790
server/app/scripts/seed.py Normal file
View File

@@ -0,0 +1,790 @@
from pathlib import Path
from mysql.connector import MySQLConnection
from mysql.connector.cursor import MySQLCursor
from app.constants import (
ALBUMS_TABLE,
ART_MEDIUM_TABLE,
ARTICLES_TABLE,
ARTISTS_TABLE,
ARTWORK_TABLE,
BIO_CONTENT_TABLE,
QUOTES_TABLE,
SERVICES_TABLE,
SOCIAL_TABLE,
VIDEOS_TABLE,
)
from app.db.conn import connect_db
from app.model import Album, Artist, Artwork, Medium, Quote, Video
from app.model.articles import Article
from app.model.bio import Bio, ProfessionalService, SocialUrl
# videos
videos: list[Video] = [
Video(
title="Human",
subtitle="(Official Music Video)",
description="<p>Song and Video Written, Performed, Produced, Recorded, Filmed and Edited by Megan Johns.</p>",
embedded_player_iframe="""<iframe width="560" height="315" src="https://www.youtube.com/embed/HiskLzZHX48?si=yomwnyr4FkupXU1e" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>""",
source="https://youtu.be/HiskLzZHX48?si=WA30xIpf1iSfJvPt", # type: ignore
),
Video(
title="EQUALITY IS HERE".title(),
subtitle="(Dark Comedy)",
description="<p>Song by Megan Johns Video Written, Directed, Produced, Storyboarded, and Edited by Megan Johns</p>",
embedded_player_iframe="""<iframe width="560" height="315" src="https://www.youtube.com/embed/TvXPCKv7eg4?si=AOrnPvhDS1NMD0tQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>""",
source="https://youtu.be/TvXPCKv7eg4?si=kpUFyN3TM-FjJ67B", # type: ignore
),
Video(
title="Feathers",
subtitle="(34 Minute Dark Fantasy Drama)",
description="<p>Written, Directed, Produced, Storyboarded, Co-Scored and Co-Edited by Megan Johns Starring Sara Blythe Visual Effects by Dylan Keim Produced by MoonWish Productions, Tendrile Productions, BluryFilms, One Tree Productions LLC, and Oros Productions Director of Photography Walter King Score by Megan Johns and Shannon Swords (Bubble Bubble Gum Gum)</p>",
embedded_player_iframe="""<iframe width="560" height="315" src="https://www.youtube.com/embed/tk8LlaAwgCg?si=olWVWRrhVEVxhFKp" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>""",
source="https://youtu.be/tk8LlaAwgCg?si=hvREJdL6DZ3CkySb", # type: ignore
website="https://feathersmovie.weebly.com/", # type: ignore
),
Video(
title="Gemini",
subtitle="(Official Music Video) (From 2019 Movie 'Live Free Or Die')",
description="<p>Song Written by Megan Johns Video Directed by Chelsea Real Video Edited by Megan Johns (MoonWish Productions) and One Tree Productions LLC</p>",
embedded_player_iframe="""<iframe width="560" height="315" src="https://www.youtube.com/embed/pWCTmCl9Im4?si=CORUdH3m5SS57nmU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>""",
source="https://www.youtube.com/watch?v=pWCTmCl9Im4", # type: ignore
),
Video(
title="Talk of Dreams",
subtitle="(Official Music Video)",
description="<p>Video Directed and Edited by Rick Gates (Tendrile Productions) Camera Operated by Rick Gates, James Kaiser, and Nick Pemble</p>",
embedded_player_iframe="""<iframe width="560" height="315" src="https://www.youtube.com/embed/yhXF5F_4F_k?si=o3kdxe9cRQXoHH0Y" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>""",
source="https://youtu.be/yhXF5F_4F_k?si=MIi0uMHhg1kTYDYe", # type: ignore
),
Video(
title="Still",
subtitle="(Official Music Video)",
description="<p>Song Written by Megan Johns Video Directed and Edited by Matt HarsH & Sam Ambler Director of Photography Mark Spomer</p>",
embedded_player_iframe="""<iframe width="560" height="315" src="https://www.youtube.com/embed/USNr1pJRXR4?si=DpHG_wR6BGaNlT3o" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>""",
source="https://www.youtube.com/watch?v=USNr1pJRXR4", # type: ignore
),
Video(
title="By the Way",
subtitle="(EFS 72 Hr Music Video Contest Entry)",
description="<p>Song Written by Megan Johns Video Written, Directed and Edited Whitney Peterson Filmed by Whitney Peterson & Cody Van Roberts</p>",
embedded_player_iframe="""<iframe src="https://player.vimeo.com/video/167692299?h=40de8a4ade" width="640" height="360" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>""",
source="https://www.youtube.com/watch?v=RFad185zIYw", # type: ignore
),
Video(
title="Sunday Drive",
subtitle="(Official Music Video)",
description="<p>Song Written by Megan Johns Video Directed, Filmed and Edited by Garrick Nelson</p>",
embedded_player_iframe="""<iframe width="560" height="315" src="https://www.youtube.com/embed/cX8K4BJbWWg?si=vSE8HNVJbBH54YQL" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>""",
source="https://www.youtube.com/watch?v=cX8K4BJbWWg", # type: ignore
),
Video(
title="Hey, Lonely",
subtitle="(Official Music Video)",
description="<p>Song Written by Megan Johns Video Directed, Filmed and Edited by James Triechler</p>",
embedded_player_iframe="""<iframe width="560" height="315" src="https://www.youtube.com/embed/AmrUTugnP2s?si=9XRy3D7hi7GHq_qV" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>""",
source="https://youtu.be/AmrUTugnP2s?si=sGPhNkYynj2iyPnt", # type: ignore
),
Video(
title="Moonwish - Gemini",
subtitle="(Electronic Version)",
description="<p>Song Written by Megan Johns</p><p>Video Directed, Filmed and Edited by John Isberg</p><p>Video Produced by Matt HarsH, John Isberg and Megan Johns</p>",
embedded_player_iframe="""<iframe src="https://player.vimeo.com/video/143818518?h=2159fc0374" width="640" height="360" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>""",
source="https://vimeo.com/143818518?embedded=true&source=vimeo_logo&owner=31047464", # type: ignore
),
Video(
title="CAFÉ Song".title(),
subtitle="(Official Music Video)",
description="<p>Song Written, Performed, and Produced by Megan Johns Video Directed, Filmed and Edited by Matt HarsH</p>",
embedded_player_iframe="""<iframe width="560" height="315" src="https://www.youtube.com/embed/llp2oi3zb_8?si=LrgxWYwmbHgBi_Es" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>""",
source="https://www.youtube.com/watch?v=llp2oi3zb_8&t=10s", # type: ignore
),
Video(
title="THE BEAT WAS BURNT".title(),
subtitle="(Official Music Video)",
description="Song Written by Megan Johns Video Directed by Matt HarsH and Filmed by Sam Ambler",
embedded_player_iframe="""<iframe width="560" height="315" src="https://www.youtube.com/embed/5fDfH7XD92s?si=hULHJgCfe_iRrdiS" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>""",
source="https://www.youtube.com/watch?v=5fDfH7XD92s", # type: ignore
),
]
# quotes
quotes = [
Quote(
body="Something like Trent Reznor taking over the Smashing Pumpkins and replacing Corgan with Tori Amos.",
author="Middle Tennessee Music",
source="https://www.indiemusicdiscovery.com/megan-johns-says-hey-lonely/", # type: ignore
),
Quote(
body="Melodic and hypnotizing. | Best Local Singer-Songwriter 2013",
author="Smile Politely",
source="http://www.smilepolitely.com/music/best_music_2013/", # type: ignore
),
Quote(
body="Tight aggressive power chords underpin Johns's raspy yet melodic singing voice, telling a story in impressionistic fragments rather than a single narrative.",
author="Eugene Magazine",
),
Quote(
body="Johns creates music that is at once sweet and biting: the languid tone of her voice providing a perfect lace overlay to the harder-rocking arrangements beneath them.",
author="The News Gazette",
),
Quote(
body="She lures her listeners to unearth a deeper subconscious level.",
author="The Buzz Weekly",
),
]
# Social & Bio information
socials = [
SocialUrl(
social_name="itunes",
social_url="https://itunes.apple.com/us/artist/megan-johns/74351585", # type: ignore
),
SocialUrl(
social_name="facebook",
social_url="http://www.facebook.com/meganjohnsmusic", # type: ignore
),
SocialUrl(
social_name="soundcloud",
social_url="https://soundcloud.com/meganjohns", # type: ignore
),
SocialUrl(
social_name="youtube",
social_url="http://youtube.com/user/MeganJohnsVideos", # type: ignore
),
SocialUrl(
social_name="instagram",
social_url="https://instagram.com/meganjohnsmusic/", # type: ignore
),
SocialUrl(
social_name="spotify",
social_url="https://open.spotify.com/artist/3CTUWD06ndDSuuUUJHm1bf", # type: ignore
),
SocialUrl(
social_name="bandcamp",
social_url="https://meganjohns.bandcamp.com/track/i-am-old", # type: ignore
),
]
services = [
ProfessionalService(service_name="Music Composition & Performance"),
ProfessionalService(service_name="Audio Engineering"),
ProfessionalService(service_name="Voice Over & Acting"),
ProfessionalService(service_name="Videography / Filmmaking"),
ProfessionalService(service_name="Visual Art"),
ProfessionalService(service_name="Classes"),
]
bio_html_content = ""
ROOT_DIR = Path(__file__).parent
HTML_FILE = ROOT_DIR / "bio.html"
with open(HTML_FILE, "r") as html_file:
for line in html_file.readlines():
bio_html_content += line.strip()
bio = Bio(name="Megan Johns", bio=bio_html_content, social_urls=socials)
# art mediums
arcylic = Medium(medium_name="Acrylic on Canvas")
oil = Medium(medium_name="Oil on Canvas")
zinc = Medium(medium_name="Zinc Plate Etching")
charcol = Medium(medium_name="Charcol on Paper")
rice_paper = Medium(medium_name="Pen and Ink on Rice Paper")
watercolor = Medium(medium_name="Watercolor on Paper En Plein Air")
illustrator_poster = Medium(medium_name="Illustrator Poster")
graphic_design = Medium(medium_name="Graphic Design")
mediums = [
arcylic,
oil,
zinc,
charcol,
rice_paper,
watercolor,
illustrator_poster,
graphic_design,
]
# artwork
# https://docs.google.com/spreadsheets/d/1RMAGgpEAjEL6Kf-QGTKxuZXZD5Cqy7NVG2pnAPctcdw/edit?gid=0#gid=0
artwork: list[Artwork] = [
Artwork(
artwork_name="Gas Mask Study",
medium=oil,
source="https://res.cloudinary.com/dreftv0ue/image/upload/v1721007132/MeganJohns/Art/Gaslit_Megan_Johns__Acrylic_on_Canvas_2009_ccge5s.jpg", # type: ignore
thumbnail="https://res.cloudinary.com/dreftv0ue/image/upload/v1721010768/MeganJohns/Art/thumbnails/Gaslit_Megan_Johns__Acrylic_on_Canvas_2009_ccge5s-400x311_aplzhd.jpg", # type: ignore
release_year=2009,
size='11"x14"',
),
Artwork(
artwork_name="Women's March",
is_featured=True,
medium=arcylic,
source="https://res.cloudinary.com/dreftv0ue/image/upload/v1720988650/MeganJohns/Art/Women_s_March_Painting_24x36_Acrylic_on_Canvas_2017_Megan_Johns_dct1vp.jpg", # type: ignore
thumbnail="https://res.cloudinary.com/dreftv0ue/image/upload/v1721010773/MeganJohns/Art/thumbnails/Women_s_March_Painting_24x36_Acrylic_on_Canvas_2017_Megan_Johns_dct1vp-400x264_i3p4mx.jpg", # type: ignore
release_year=2017,
size='24"x36"',
),
Artwork(
artwork_name="Spilled Beans Study",
source="https://res.cloudinary.com/dreftv0ue/image/upload/v1720992412/MeganJohns/Art/Spilled_Beans_Signature_2019_q81yvi.png", # type: ignore
thumbnail="https://res.cloudinary.com/dreftv0ue/image/upload/v1721010768/MeganJohns/Art/thumbnails/Spilled_Beans_Signature_2019_q81yvi-400x400_daovzb.png", # type: ignore
release_year=2019,
),
Artwork(
artwork_name="Captain",
medium=zinc,
source="https://res.cloudinary.com/dreftv0ue/image/upload/v1718391476/MeganJohns/Art/Captain_9x12_Zinc_Plate_Etching_2009_zxh2wm.jpg", # type: ignore
thumbnail="https://res.cloudinary.com/dreftv0ue/image/upload/v1721010768/MeganJohns/Art/thumbnails/Captain_9x12_Zinc_Plate_Etching_2009_zxh2wm-400x267_few8in.jpg", # type: ignore
release_year=2009,
size='9"x12"',
),
Artwork(
artwork_name="Resting Chill Face",
medium=arcylic,
source="https://res.cloudinary.com/dreftv0ue/image/upload/v1720992884/MeganJohns/Art/Resting_Chill_Face_16x20_Acrylic_on_Canvas_from_Life_2019_edited_no_edges_hfw28u.jpg", # type: ignore
thumbnail="https://res.cloudinary.com/dreftv0ue/image/upload/v1721010772/MeganJohns/Art/thumbnails/Resting_Chill_Face_16x20_Acrylic_on_Canvas_from_Life_2019_edited_no_edges_hfw28u-316x400_umgpus.jpg", # type: ignore
release_year=2019,
size='16"x12"',
),
Artwork(
artwork_name="Louis XIV",
medium=arcylic,
source="https://res.cloudinary.com/dreftv0ue/image/upload/v1720988576/MeganJohns/Art/Louis_XIV_16x20_Acrylic_on_Canvas_2019_Megan_Johns_kmbirh.jpg", # type: ignore
thumbnail="https://res.cloudinary.com/dreftv0ue/image/upload/v1721010769/MeganJohns/Art/thumbnails/Louis_XIV_16x20_Acrylic_on_Canvas_2019_Megan_Johns_kmbirh-319x400_ixelam.jpg", # type: ignore
release_year=2019,
size='16"x20"',
),
Artwork(
artwork_name="Allerton",
source="https://res.cloudinary.com/dreftv0ue/image/upload/v1720988504/MeganJohns/Art/Allerton_FU_Dog_Watercolor_on_Paper_En_Plein_Air_no_edges_cmkbjs.jpg", # type: ignore
thumbnail="https://res.cloudinary.com/dreftv0ue/image/upload/v1721010771/MeganJohns/Art/thumbnails/Allerton_FU_Dog_Watercolor_on_Paper_En_Plein_Air_no_edges_cmkbjs-300x400_odzydz.jpg", # type: ignore
),
Artwork(
artwork_name="Goat and Rooster",
medium=arcylic,
source="https://res.cloudinary.com/dreftv0ue/image/upload/v1720988474/MeganJohns/Art/Goat_and_Rooster_16x20_Acrylic_on_Canvas_2019_Np_edges_d2y7hk.jpg", # type: ignore
thumbnail="https://res.cloudinary.com/dreftv0ue/image/upload/v1721010771/MeganJohns/Art/thumbnails/Goat_and_Rooster_16x20_Acrylic_on_Canvas_2019_Np_edges_d2y7hk-314x400_uhskcy.jpg", # type: ignore
release_year=2019,
size='16"x20"',
),
Artwork(
artwork_name="Feathers Poster",
medium=graphic_design,
source="https://res.cloudinary.com/dreftv0ue/image/upload/v1718413105/MeganJohns/Art/Feathers_Poster_Graphic%20Design_2020_hcjej2.jpg", # type: ignore
thumbnail="https://res.cloudinary.com/dreftv0ue/image/upload/v1721010771/MeganJohns/Art/thumbnails/Feathers_Poster_Graphic_20Design_2020_hcjej2-270x400_zwzwd3.jpg", # type: ignore
release_year=2020,
),
Artwork(
artwork_name="Hey Lonely",
source="https://res.cloudinary.com/dreftv0ue/image/upload/v1718391586/MeganJohns/Art/Hey_Lonely_Photoshopped_Photography_2012_h1aitf.jpg", # type: ignore
thumbnail="https://res.cloudinary.com/dreftv0ue/image/upload/v1721010773/MeganJohns/Art/thumbnails/Hey_Lonely_Photoshopped_Photography_2012_h1aitf-400x400_l0bghw.jpg", # type: ignore
release_year=2012,
),
Artwork(
artwork_name="Teapot and Teacup",
source="https://res.cloudinary.com/dreftv0ue/image/upload/v1718391538/MeganJohns/Art/Teapot_and_Teacup_Acrylic_on_Canvas_2017_r6ivpo.jpg", # type: ignore
thumbnail="https://res.cloudinary.com/dreftv0ue/image/upload/v1721010769/MeganJohns/Art/thumbnails/Teapot_and_Teacup_Acrylic_on_Canvas_2017_r6ivpo-400x233_ztsjtv.jpg", # type: ignore
release_year=2017,
),
Artwork(
artwork_name="Polyphemus Moth",
source="https://res.cloudinary.com/dreftv0ue/image/upload/v1718391523/MeganJohns/Art/Polyphemus_Moth_16x16_Acrylic_on_Canvas_Megan_Johns_hgqc1h.jpg", # type: ignore
thumbnail="https://res.cloudinary.com/dreftv0ue/image/upload/v1721010771/MeganJohns/Art/thumbnails/Polyphemus_Moth_16x16_Acrylic_on_Canvas_Megan_Johns_hgqc1h-400x400_vlnju2.jpg", # type: ignore
size='16"x16"',
),
Artwork(
artwork_name="Lou",
medium=arcylic,
release_year=2019,
source="https://res.cloudinary.com/dreftv0ue/image/upload/v1718391496/MeganJohns/Art/Lou_Acrylic_on_Canvas_2019_fze7qc.jpg", # type: ignore
thumbnail="https://res.cloudinary.com/dreftv0ue/image/upload/v1721010769/MeganJohns/Art/thumbnails/Lou_Acrylic_on_Canvas_2019_fze7qc-320x400_cf5ed9.jpg", # type: ignore
),
Artwork(
artwork_name="Silver Teapot",
source="https://res.cloudinary.com/dreftv0ue/image/upload/v1718349058/MeganJohns/Art/Silver_Teapot_12x12_Acrylic_on_Canvas_2017_Megan_Johns_zfrknd.jpg", # type: ignore
thumbnail="https://res.cloudinary.com/dreftv0ue/image/upload/v1721010771/MeganJohns/Art/thumbnails/Silver_Teapot_12x12_Acrylic_on_Canvas_2017_Megan_Johns_zfrknd-400x400_tz6ovb.jpg", # type: ignore
release_year=2017,
size='12"x12"',
),
Artwork(
artwork_name="Gemini",
source="https://res.cloudinary.com/dreftv0ue/image/upload/v1718347980/MeganJohns/Art/Gemini_Single_Album_Cover_Photoshoped_Photography__2015_Megan_Johns_v4zwao.jpg", # type: ignore
thumbnail="https://res.cloudinary.com/dreftv0ue/image/upload/v1721010769/MeganJohns/Art/thumbnails/Gemini_Single_Album_Cover_Photoshoped_Photography__2015_Megan_Johns_v4zwao-400x400_a7kgs5.jpg", # type: ignore
release_year=2015,
),
Artwork(
artwork_name="Glass Ceiling",
source="https://res.cloudinary.com/dreftv0ue/image/upload/v1718347979/MeganJohns/Art/Glass_Cieling_Self_Portrait_Acrylic_on_Canvas_2009_Megan_Johns_jsgupl.jpg", # type: ignore
thumbnail="https://res.cloudinary.com/dreftv0ue/image/upload/v1721010769/MeganJohns/Art/thumbnails/Glass_Cieling_Self_Portrait_Acrylic_on_Canvas_2009_Megan_Johns_jsgupl-400x269_luwr5i.jpg", # type: ignore
release_year=2009,
),
]
# news articles
# TODO
articles: list[Article] = []
# album artists
megan_artist = Artist(
artist_name="Megan Johns",
artist_url="https://music.apple.com/us/artist/megan-johns/74351585", # type: ignore
)
greytones = Artist(
artist_name="The Greytones",
artist_url="https://open.spotify.com/artist/5JyA3JrRMinqhLslj7EyLl", # type: ignore
)
bubble_bubble_bum_bum = Artist(
artist_name="Bubble Bubble Gum Gum",
artist_url="https://music.apple.com/us/artist/bubble-bubble-gum-gum/1554007615", # type: ignore
)
artists: list[Artist] = [megan_artist, greytones, bubble_bubble_bum_bum]
# discography
albums: list[Album] = [
Album(
album_name="Dirty Shoes",
release_year=2005,
artist=megan_artist,
apple_music_url="https://music.apple.com/us/album/dirty-shoes/74351635", # type: ignore
bandcamp_url="https://meganjohns.bandcamp.com/track/fog", # type: ignore
front_artwork_url="https://res.cloudinary.com/dreftv0ue/image/upload/v1717363942/MeganJohns/Discography%20-%20Album%20%2B%20Single%20Covers/Dirty_Shoes_Cover_2005_bnb8u0.jpg", # type: ignore
bandcamp_player='<iframe style="border: 0; width: 100%; height: 120px;" src="https://bandcamp.com/EmbeddedPlayer/track=2275072448/size=large/bgcol=ffffff/linkcol=0687f5/tracklist=false/artwork=small/transparent=true/" seamless><a href="https://meganjohns.bandcamp.com/track/fog">Fog by Megan Johns</a></iframe>',
),
Album(
album_name="Hey, Lonely",
release_year=2012,
artist=megan_artist,
apple_music_url="https://music.apple.com/us/album/hey-lonely/565637660", # type: ignore
front_artwork_url="https://res.cloudinary.com/dreftv0ue/image/upload/v1717363966/MeganJohns/Discography%20-%20Album%20%2B%20Single%20Covers/Hey_Lonely_Jacket_hires-2_kwo34f.jpg", # type: ignore
rear_artwork_url="https://res.cloudinary.com/dreftv0ue/image/upload/v1717364028/MeganJohns/Discography%20-%20Album%20%2B%20Single%20Covers/Hey_Lonely_Reverse2_esmjia.jpg", # type: ignore
bandcamp_player='<iframe style="border: 0; width: 100%; height: 120px;" src="https://bandcamp.com/EmbeddedPlayer/album=2623205502/size=large/bgcol=ffffff/linkcol=0687f5/tracklist=false/artwork=small/transparent=true/" seamless><a href="https://meganjohns.bandcamp.com/album/hey-lonely">Hey, Lonely by Megan Johns</a></iframe>',
),
Album(
album_name="Gemini",
release_year=2015,
artist=megan_artist,
bandcamp_url="https://meganjohns.bandcamp.com/track/gemini", # type: ignore
front_artwork_url="https://res.cloudinary.com/dreftv0ue/image/upload/v1717363956/MeganJohns/Discography%20-%20Album%20%2B%20Single%20Covers/Gemini_MoonWish_Single_2015_spgv2m.jpg", # type: ignore
bandcamp_player='<iframe style="border: 0; width: 100%; height: 120px;" src="https://bandcamp.com/EmbeddedPlayer/track=2493215358/size=large/bgcol=ffffff/linkcol=0687f5/tracklist=false/artwork=small/transparent=true/" seamless><a href="https://meganjohns.bandcamp.com/track/gemini">Gemini by Moonwish</a></iframe>',
),
Album(
album_name="Inner Voice",
release_year=2019,
artist=megan_artist,
bandcamp_url="https://meganjohns.bandcamp.com/album/inner-voice-ep", # type: ignore
front_artwork_url="https://res.cloudinary.com/dreftv0ue/image/upload/v1717363981/MeganJohns/Discography%20-%20Album%20%2B%20Single%20Covers/Inner_Voice_EP_2019_po0nbq.jpg", # type: ignore
rear_artwork_url="https://res.cloudinary.com/dreftv0ue/image/upload/v1717363984/MeganJohns/Discography%20-%20Album%20%2B%20Single%20Covers/Inner_Voice_EP_Backcover_2019_ilenfq.jpg", # type: ignore
bandcamp_player='<iframe style="border: 0; width: 100%; height: 120px;" src="https://bandcamp.com/EmbeddedPlayer/album=1645073059/size=large/bgcol=ffffff/linkcol=0687f5/tracklist=false/artwork=small/transparent=true/" seamless><a href="https://meganjohns.bandcamp.com/album/inner-voice-ep">Inner Voice EP by Megan Johns</a></iframe>',
),
Album(
album_name="MoonWish Recordings 2015",
release_year=2015,
artist=megan_artist,
bandcamp_url="https://meganjohns.bandcamp.com/album/moonwish-recordings-2015", # type: ignore
front_artwork_url="https://res.cloudinary.com/dreftv0ue/image/upload/v1717363988/MeganJohns/Discography%20-%20Album%20%2B%20Single%20Covers/MoonWish_Recordings_2015_j3pzyd.jpg", # type: ignore
bandcamp_player='<iframe style="border: 0; width: 100%; height: 120px;" src="https://bandcamp.com/EmbeddedPlayer/album=3211709152/size=large/bgcol=ffffff/linkcol=0687f5/tracklist=false/artwork=small/transparent=true/" seamless><a href="https://meganjohns.bandcamp.com/album/moonwish-recordings-2015">MoonWish Recordings 2015 by Megan Johns</a></iframe>',
),
Album(
album_name="Penumbra",
release_year=2007,
artist=greytones,
spotify_url="https://open.spotify.com/album/1FMhRBPjhCOe21JNwgYUAb", # type: ignore
front_artwork_url="https://res.cloudinary.com/dreftv0ue/image/upload/v1717363997/MeganJohns/Discography%20-%20Album%20%2B%20Single%20Covers/The_Greytones_Penumbra_Cover_2007_rh02wu.jpg", # type: ignore
),
Album(
album_name="Feathers",
release_year=2021,
artist=bubble_bubble_bum_bum,
spotify_url="https://music.apple.com/us/album/feathers-original-motion-picture-score/1554024529", # type: ignore
front_artwork_url="https://res.cloudinary.com/dreftv0ue/image/upload/v1717363949/MeganJohns/Discography%20-%20Album%20%2B%20Single%20Covers/Feathers_Score_Bubble_Bubble_Gum_Gum_2021_itjio0.jpg", # type: ignore
),
Album(
album_name="Human",
release_year=2022,
artist=megan_artist,
bandcamp_url="https://meganjohns.bandcamp.com/track/human", # type: ignore
front_artwork_url="https://res.cloudinary.com/dreftv0ue/image/upload/v1717363973/MeganJohns/Discography%20-%20Album%20%2B%20Single%20Covers/Human_Single_2022_lknlpc.png", # type: ignore
bandcamp_player='<iframe style="border: 0; width: 100%; height: 120px;" src="https://bandcamp.com/EmbeddedPlayer/track=3620590740/size=large/bgcol=ffffff/linkcol=0687f5/tracklist=false/artwork=small/transparent=true/" seamless><a href="https://meganjohns.bandcamp.com/track/human">Human by Megan Johns</a></iframe>',
),
Album(
album_name="I Am Old",
release_year=2022,
artist=megan_artist,
bandcamp_url="https://meganjohns.bandcamp.com/track/i-am-old", # type: ignore
front_artwork_url="https://res.cloudinary.com/dreftv0ue/image/upload/v1717363976/MeganJohns/Discography%20-%20Album%20%2B%20Single%20Covers/I_Am_Old_Single_2022_mao2yw.jpg", # type: ignore
bandcamp_player='<iframe style="border: 0; width: 100%; height: 120px;" src="https://bandcamp.com/EmbeddedPlayer/track=2078638263/size=large/bgcol=ffffff/linkcol=0687f5/tracklist=false/artwork=small/transparent=true/" seamless><a href="https://meganjohns.bandcamp.com/track/i-am-old">I Am Old by Megan Johns</a></iframe>',
),
]
def get_db_cursor() -> tuple[MySQLConnection, MySQLCursor]:
db = connect_db()
cursor = db.cursor()
return db, cursor
def close_db_cursor(db: MySQLConnection, cursor: MySQLCursor) -> None:
db.commit()
cursor.close()
db.close()
def drop_tables():
db, cursor = get_db_cursor()
for table in [
ALBUMS_TABLE,
ARTISTS_TABLE,
ARTICLES_TABLE,
ARTWORK_TABLE,
ART_MEDIUM_TABLE,
BIO_CONTENT_TABLE,
SOCIAL_TABLE,
QUOTES_TABLE,
VIDEOS_TABLE,
SERVICES_TABLE,
]:
print(f"dropping table: {table}")
cursor.execute(
f"""-- sql
DROP TABLE IF EXISTS {table};
"""
)
close_db_cursor(db, cursor)
print("")
def seed_albums() -> None:
print(f"seeding {len(albums)} albums")
db, cursor = get_db_cursor()
cursor.execute(
f"""-- sql
CREATE TABLE {ALBUMS_TABLE} (
id INT NOT NULL AUTO_INCREMENT,
album_name VARCHAR(255) NOT NULL,
release_year YEAR,
artist_id INT,
spotify_url VARCHAR(255),
itunes_url VARCHAR(255),
bandcamp_url VARCHAR(255),
apple_music_url VARCHAR(255),
front_artwork_url VARCHAR(255),
rear_artwork_url VARCHAR(255),
bandcamp_player VARCHAR(1024),
PRIMARY KEY (id),
FOREIGN KEY (artist_id) REFERENCES {ARTISTS_TABLE}(id) ON DELETE CASCADE
);
"""
)
for album in albums:
cursor.execute(
f"""-- sql
INSERT INTO {ALBUMS_TABLE} (album_name, release_year, artist_id, spotify_url, itunes_url, bandcamp_url, apple_music_url, front_artwork_url, rear_artwork_url, bandcamp_player)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s);
""",
(
album.album_name,
album.release_year,
album.artist.id,
str(album.spotify_url) if album.spotify_url else None,
str(album.itunes_url) if album.itunes_url else None,
str(album.bandcamp_url) if album.bandcamp_url else None,
str(album.apple_music_url) if album.apple_music_url else None,
str(album.front_artwork_url) if album.front_artwork_url else None,
str(album.rear_artwork_url) if album.rear_artwork_url else None,
album.bandcamp_player,
),
)
if cursor.lastrowid:
album.id = cursor.lastrowid
db.commit()
cursor.close()
db.close()
def seed_quotes() -> None:
print(f"seeding {len(quotes)} quotes")
db, cursor = get_db_cursor()
cursor.execute(
f"""-- sql
CREATE TABLE {QUOTES_TABLE} (
id INT NOT NULL AUTO_INCREMENT,
body VARCHAR(1000) NOT NULL,
author VARCHAR(255) NOT NULL,
source VARCHAR(255),
PRIMARY KEY (id)
);
"""
)
for quote in quotes:
cursor.execute(
f"""-- sql
INSERT INTO {QUOTES_TABLE} (body, author, source)
VALUES (%s, %s, %s);
""",
(quote.body, quote.author, str(quote.source) if quote.source else None),
)
if cursor.lastrowid:
quote.id = cursor.lastrowid
close_db_cursor(db, cursor)
def seed_artists() -> None:
print(f"seeding {len(artists)} artists")
db, cursor = get_db_cursor()
cursor.execute(
f"""-- sql
CREATE TABLE {ARTISTS_TABLE} (
id INT NOT NULL AUTO_INCREMENT,
artist_name VARCHAR(255) NOT NULL,
artist_url VARCHAR(255) NOT NULL,
PRIMARY KEY (id)
);
"""
)
for artist in artists:
cursor.execute(
f"""-- sql
INSERT INTO {ARTISTS_TABLE} (artist_name, artist_url)
VALUES (%s, %s);
""",
(artist.artist_name, str(artist.artist_url)),
)
if cursor.lastrowid:
artist.id = cursor.lastrowid
close_db_cursor(db, cursor)
def seed_articles():
print(f"seeding {len(articles)} articles")
db = connect_db()
cursor = db.cursor()
cursor.execute(
f"""-- sql
CREATE TABLE articles (
id INT NOT NULL AUTO_INCREMENT,
article_title VARCHAR(255) NOT NULL,
body VARCHAR(255) NOT NULL,
video_url VARCHAR(255),
is_featured BOOLEAN DEFAULT FALSE,
PRIMARY KEY (id)
);
"""
)
for article in articles:
cursor.execute(
f"""-- sql
INSERT INTO {ARTICLES_TABLE} (article_title, body, video_url, is_featured)
VALUES (%s, %s, %s, %s)
""",
(
article.article_title,
article.body,
str(article.video_url),
article.is_featured,
),
)
if cursor.lastrowid:
article.id = cursor.lastrowid
db.commit()
cursor.close()
db.close()
def seed_artwork():
print(f"seeding {len(artwork)} pieces of art")
db = connect_db()
cursor = db.cursor()
cursor.execute(
f"""-- sql
CREATE TABLE {ARTWORK_TABLE} (
id INT NOT NULL AUTO_INCREMENT,
medium_id INT,
artwork_name VARCHAR(255) UNIQUE NOT NULL,
source VARCHAR(255) NOT NULL,
thumbnail VARCHAR(255) NOT NULL,
is_featured BOOLEAN DEFAULT FALSE,
release_year YEAR,
size VARCHAR(255),
PRIMARY KEY (id),
FOREIGN KEY (medium_id) REFERENCES {ART_MEDIUM_TABLE}(id) ON DELETE CASCADE
);
"""
)
for work in artwork:
cursor.execute(
f"""-- sql
INSERT INTO {ARTWORK_TABLE} (medium_id, artwork_name, source, thumbnail, is_featured, release_year, size)
VALUES (%s, %s, %s, %s, %s, %s, %s);
""",
(
work.medium.id if work.medium is not None else None,
work.artwork_name,
str(work.source),
str(work.thumbnail),
work.is_featured,
work.release_year,
work.size,
),
)
if cursor.lastrowid:
work.id = cursor.lastrowid
close_db_cursor(db, cursor)
def seed_mediums():
print(f"seeding {len(mediums)} art mediums")
db, cursor = get_db_cursor()
cursor.execute(
f"""-- sql
CREATE TABLE {ART_MEDIUM_TABLE} (
id INT NOT NULL AUTO_INCREMENT,
medium_name VARCHAR(255),
PRIMARY KEY (id)
);
"""
)
for medium in mediums:
cursor.execute(
f"""-- sql
INSERT INTO {ART_MEDIUM_TABLE} (medium_name)
VALUES (%s);
""",
(medium.medium_name,),
)
if cursor.lastrowid:
medium.id = cursor.lastrowid
close_db_cursor(db, cursor)
def seed_social_info():
print(f"seeding {len(socials)} social urls")
db, cursor = get_db_cursor()
cursor.execute(
f"""-- sql
CREATE TABLE {SOCIAL_TABLE} (
id INT NOT NULL AUTO_INCREMENT,
social_name VARCHAR(255) NOT NULL UNIQUE,
social_url VARCHAR(255) NOT NULL UNIQUE,
primary key (id)
);
"""
)
for s in socials:
cursor.execute(
f"""-- sql
INSERT INTO {SOCIAL_TABLE} (social_name, social_url)
VALUES (%s, %s);
""",
(s.social_name, str(s.social_url)),
)
if cursor.lastrowid:
s.id = cursor.lastrowid
close_db_cursor(db, cursor)
def seed_professional_services():
print(f"seeding {len(services)} professional services")
db, cursor = get_db_cursor()
cursor.execute(
f"""-- sql
CREATE TABLE {SERVICES_TABLE} (
id INT NOT NULL AUTO_INCREMENT,
service_name VARCHAR(255) NOT NULL UNIQUE,
primary key (id)
);
"""
)
for s in services:
cursor.execute(
f"""-- sql
INSERT INTO {SERVICES_TABLE} (service_name)
VALUES (%s);
""",
(s.service_name,),
)
if cursor.lastrowid:
s.id = cursor.lastrowid
close_db_cursor(db, cursor)
def seed_bio():
print(f"seeding single bio")
db, cursor = get_db_cursor()
cursor.execute(
f"""-- sql
CREATE TABLE {BIO_CONTENT_TABLE} (
id INT NOT NULL AUTO_INCREMENT,
content TEXT NOT NULL,
primary key (id)
);
"""
)
cursor.execute(
f"""-- sql
INSERT INTO {BIO_CONTENT_TABLE} (content)
VALUES (%s);
""",
(bio.bio,),
)
close_db_cursor(db, cursor)
def seed_videos() -> None:
print(f"seeding single bio")
db, cursor = get_db_cursor()
cursor.execute(
f"""-- sql
CREATE TABLE {VIDEOS_TABLE} (
id INT NOT NULL AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
subtitle VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
source VARCHAR(255) NOT NULL,
embedded_player_iframe TEXT,
website VARCHAR(255),
primary key (id)
);
"""
)
for video in videos:
cursor.execute(
f"""-- sql
INSERT INTO {VIDEOS_TABLE} (title, subtitle, description, source, embedded_player_iframe, website)
VALUES (%s, %s, %s, %s, %s, %s);
""",
(
video.title,
video.subtitle,
video.description,
str(video.source),
video.embedded_player_iframe,
str(video.website) if video.website else None,
),
)
if cursor.lastrowid:
video.id = cursor.lastrowid
close_db_cursor(db, cursor)
def main() -> None:
drop_tables()
seed_artists()
seed_albums()
seed_articles()
seed_mediums()
seed_artwork()
seed_social_info()
seed_bio()
seed_quotes()
seed_videos()
seed_professional_services()