diff --git a/conf.toml b/conf.toml
index f27ef16..acc34c2 100644
--- a/conf.toml
+++ b/conf.toml
@@ -4,12 +4,18 @@ error_log = "error.log"
 # Quality can be one of "master, lossless, high and low"
 # Keep in mind that you can only get the quality included in your tidal sub
 quality = "lossless"
-user_id = 188721652
+user_id =
 
 dest_dir = "./downloads/"
+state_dir = "./state/"
 # The following templates are passed an artist, album and track object.
 # Possible attributes can be found here: https://tidalapi.netlify.app/api.html
 # The artist is derived from the album a track is in rather than the track itself.
-dl_dir ="/tmp/tidal-scraper/{track.name}.part"
 album_dir = "{artist.name}/{album.name}/"
+playlist_dir = "{playlist.name}/"
 track_name = "{track.track_num}: {track.name}"
+
+# One of 160, 320, 480, 640, 750, 1080
+playlist_image_size = 1080
+# One of 80, 160, 320, 640, 1280
+album_image_size = 1280
diff --git a/requirements.txt b/requirements.txt
index 55fc852..162be56 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
 git+https://github.com/tamland/python-tidal
 pycrypto
 tqdm
+mutagen
diff --git a/tidal-scraper/account.py b/tidal-scraper/account.py
deleted file mode 100644
index ba549d6..0000000
--- a/tidal-scraper/account.py
+++ /dev/null
@@ -1,58 +0,0 @@
-import json
-from tidalapi import session, user, playlist, media, album, artist
-from helper import CONF
-
-ALBUM = 1
-ARTIST = 2
-PLAYLIST = 3
-TRACK = 4
-
-
-
-class account:
-    def __init__(self, user_id: int, quality: str):
-        match quality:
-            case "master":
-                q = session.Quality.master
-            case "lossless":
-                q = session.Quality.lossless
-            case "high":
-                q = session.Quality.high
-            case "low":
-                q = session.Quality.low
-            case _:
-                raise Exception("Quality misconfigured in conf.toml")
-        config = session.Config(quality=q)
-        self.user_id = user_id
-        self.session = session.Session(config)
-        self.favorites = user.Favorites(self.session, CONF['user_id'])
-        self._state = {
-            "albums": {},
-            "artists": {},
-            "playlists": {},
-            "tracks": {},
-        }
-
-    @staticmethod
-    def create_obj_state(
-        obj: playlist.Playlist | media.Track | album.Album | artist.Artist,
-        downloaded: bool = False,
-    ) -> dict[str, int | bool]:
-
-        match type(obj):
-            case album.Album:
-                obj_type = ALBUM
-            case artist.Artist:
-                obj_type = ARTIST
-            case playlist.Playlist:
-                obj_type = PLAYLIST
-            case media.Track:
-                obj_type = TRACK
-            case _:
-                raise Exception("Incorrect object type received")
-
-        return {
-            "id": obj.id,
-            "type": obj_type,
-            "downloaded": downloaded,
-        }
diff --git a/tidal-scraper/download.py b/tidal-scraper/download.py
index 06e6087..1f7942a 100644
--- a/tidal-scraper/download.py
+++ b/tidal-scraper/download.py
@@ -1,25 +1,30 @@
-import helper
-from helper import CONF, EXTENSIONS
 import metadata
+from helper import CONF, EXTENSIONS, clean_template, log_error
 
 import tidalapi
 import os
 import json
-
+import requests
+import io
+from tqdm import tqdm
 from base64 import b64decode
 from Crypto.Cipher import AES
 from Crypto.Util import Counter
-from typing import Tuple, BinaryIO
-from shutil import move
+from typing import Tuple
+from typing import BinaryIO
+
+# MASTER_KEY = b64decode("UIlTTEMmmLfGowo/UC60x2H45W6MdGgTRfo/umg4754=")
+MASTER_KEY = (
+    b"P\x89SLC&\x98\xb7\xc6\xa3\n?P.\xb4\xc7a\xf8\xe5n\x8cth\x13E\xfa?\xbah8\xef\x9e"
+)
 
 
-def __decode_key_id(key_id) -> Tuple[bytes, bytes]:
-    master_key = b64decode("UIlTTEMmmLfGowo/UC60x2H45W6MdGgTRfo/umg4754=")
+def __decode_key_id(key_id: str) -> Tuple[bytes, bytes]:
     decoded_key_id = b64decode(key_id)
     init_vector = decoded_key_id[:16]
     encrypted_token = decoded_key_id[16:]
 
-    decryptor = AES.new(master_key, AES.MODE_CBC, init_vector)
+    decryptor = AES.new(MASTER_KEY, AES.MODE_CBC, init_vector)
     decrypted_token = decryptor.decrypt(encrypted_token)
 
     key = decrypted_token[:16]
@@ -28,35 +33,34 @@ def __decode_key_id(key_id) -> Tuple[bytes, bytes]:
     return key, nonce
 
 
-def __decrypt_file(
-    input_file: BinaryIO, output_file: BinaryIO, key: bytes, nonce: bytes
-) -> None:
+def __decrypt_file(fp: BinaryIO, key: bytes, nonce: bytes) -> None:
     counter = Counter.new(64, prefix=nonce, initial_value=0)
     decryptor = AES.new(key, AES.MODE_CTR, counter=counter)
-    data = decryptor.decrypt(input_file.read())
-    output_file.write(data)
+    data = decryptor.decrypt(fp)
+    fp.write(data)
 
 
-def set_metadata():
-    pass
+def __download_file(url: str, fp: BinaryIO) -> None:
+    r = requests.get(url, stream=True)
+    r.raise_for_status()
+    total_bytes = int(r.headers.get("content-length", 0))
+    progress = tqdm(total=total_bytes, unit="iB", unit_scale=True)
+    for data in r.iter_content(1024):
+        fp.write(data)
+        progress.update(len(data))
+    progress.close()
 
 
-def download_track(
-    track: tidalapi.Track,
-    album: tidalapi.Album,
-) -> None:
-    dl_path = helper.clean_template(CONF["dl_dir"])
-    dest_path = helper.clean_template(
-        CONF["dest_dir"] + CONF["album_dir"] + CONF["track_name"],
-        track=track,
-        album=album,
-        artist=album.artist,
-    )
+def download_track(track: tidalapi.Track, dest_path: str) -> None:
+    album = track.album
+    assert album
+    dest_path += clean_template(CONF["track_name"], track=track)
+
     try:
         stream = track.stream()
         manifest = json.loads(b64decode(stream.manifest))
+        print(manifest)
         url = manifest["urls"][0]
-        codec = manifest['codecs']
         for ext in EXTENSIONS:
             if ext in url and ext is not ".mp4":
                 dest_path += ext
@@ -66,45 +70,72 @@ def download_track(
                 else:
                     dest_path += ".m4a"
             if os.path.exists(dest_path + ext) and CONF["skip_downloaded"]:
-                print("Skipping downloaded song")
+                print(f"Skipping {album.artist.name} - {track.name}")
                 return
 
-        key_id = manifest.get("keyId", None)
+        assert track.name and album.name
         os.makedirs(os.path.dirname(dest_path), exist_ok=True)
-        with open(dest_path, "wb") as f:
-            # TODO: DOWNLOAD
-            pass
+        with io.BytesIO() as b:
+            print(f"Downloading {album.artist.name} - {track.name}")
+            key_id = manifest.get("keyId", None)
+            __download_file(url, b)
 
         if key_id:
-            key, nonce = __decode_key_id(key_id)
-            with open(dl_path, "rb") as i, open(dest_path, "wb") as o:
-                __decrypt_file(i, o, key, nonce)
-        else:
-            move(dl_path, dest_path)
-
-        assert track.name is not None
-        assert album.name is not None
-        metadata.write(f, codec, track.name, album.name, str(track.track_num), str(album.num_tracks))
+            __decrypt_file(b, *__decode_key_id(key_id))
+            metadata.write(
+                b,
+                manifest["codecs"],
+                track.name,
+                album.name,
+                str(track.track_num),
+                str(album.num_tracks),
+            )
+            with open(dest_path, "wb") as f:
+                data = b.read()
+                f.write(data)
     except:
-        assert album.artist is not None
-        helper.log_error(
-            "Failure while downloading {artist} - {album} - {track}:",
+        log_error(
+            "Failure while downloading {artist} - {track}",
             artist=album.artist.name,
-            album=album.name,
             track=track.name,
         )
-        # stack trace is printed by log_error()
 
 
-def download_cover(album: tidalapi.Album) -> None:
-    dest_path = helper.clean_template(
+def download_cover(
+    obj: tidalapi.Album | tidalapi.Playlist, dest_path: str, size: int
+) -> None:
+    if os.path.exists(dest_path) and CONF["skip_downloaded"]:
+        return
+
+    url = obj.image(size)
+    with open(dest_path, "wb") as f:
+        __download_file(url, f)
+
+
+def download_album(album: tidalapi.Album) -> None:
+    dest_path = clean_template(
         CONF["dest_dir"] + CONF["album_dir"],
         album=album,
         artist=album.artist,
     )
-    url = album.image(1280)
+    download_cover(album, dest_path, CONF["album_image_size"])
+    tracks = album.tracks()
+    for track in tracks:
+        download_track(track, dest_path)
 
-    if os.path.exists(dest_path) and CONF["skip_downloaded"]:
-        return
 
-    # TODO: DOWNLOAD
+def download_playlist(playlist: tidalapi.Playlist) -> None:
+    dest_path = clean_template(
+        CONF["dest_dir"] + CONF["playlist_dir"],
+        playlist=playlist,
+    )
+    download_cover(playlist, dest_path, CONF["playlist_image_size"])
+    tracks = playlist.tracks()
+    for track in tracks:
+        download_track(track, dest_path)
+
+
+def download_artist(artist: tidalapi.Artist) -> None:
+    albums = artist.get_albums()
+    for album in albums:
+        download_album(album)
diff --git a/tidal-scraper/helper.py b/tidal-scraper/helper.py
index 5668361..16c8de5 100644
--- a/tidal-scraper/helper.py
+++ b/tidal-scraper/helper.py
@@ -15,5 +15,6 @@ def clean_template(path: str, **kwargs) -> str:
 
 def log_error(template: str, **kwargs):
     with open(CONF['error_log']) as f:
-        f.write(template.format(**kwargs))
+        error = template.format(**kwargs)
+        f.write(error)
         traceback.print_exception(*sys.exc_info(), file=f)
diff --git a/tidal-scraper/metadata.py b/tidal-scraper/metadata.py
index 34a319e..7cbde92 100644
--- a/tidal-scraper/metadata.py
+++ b/tidal-scraper/metadata.py
@@ -1,9 +1,10 @@
-from typing import BinaryIO
 from mutagen import flac, mp4  # , mp3
 
 from mutagen.mp4 import MP4Tags
 from mutagen._vorbis import VCommentDict
 
+from typing import BinaryIO
+
 # from mutagen.id3._tags import ID3Tags
 
 # from mutagen.id3._frames import (
@@ -119,14 +120,16 @@ def write(
     copyright: str = "",
     cover: bytes | None = None,
     cover_mime: str | None = None,
-):
+) -> None:
     args = locals()
     # TODO: Figure out what codecs are sent in the manifest
     # WARN: This match is currently using placeholders
     match codec:
-        case "???flac???":
-            file = flac.FLAC(fp)
-            __write_flac(file, *args)
-        case "???aac???":
-            file = mp4.MP4(fp)
-            __write_mp4(file, *args)
+        case "flac":
+            f = flac.FLAC(fp)
+            __write_flac(f, *args)
+        case "aac":
+            f = mp4.MP4(fp)
+            __write_mp4(f, *args)
+        case _:
+            raise Exception(f"Couldn't recognize codec {codec}")
diff --git a/tidal-scraper/run.py b/tidal-scraper/run.py
index 0197a16..2786004 100644
--- a/tidal-scraper/run.py
+++ b/tidal-scraper/run.py
@@ -1,3 +1,14 @@
 #!/bin/env python
-import download
+from download import download_album
+from state import State
+from helper import CONF
 
+s = State(CONF['user_id'], CONF['quality'])
+
+s.login()
+
+albums = s.favorites.albums()
+
+download_album(albums[0])
+s.set_dl_state(albums[0], True)
+s.write_state()
diff --git a/tidal-scraper/state.py b/tidal-scraper/state.py
new file mode 100644
index 0000000..c6e4f1e
--- /dev/null
+++ b/tidal-scraper/state.py
@@ -0,0 +1,92 @@
+import json
+from datetime import datetime
+from tidalapi import session, user, playlist, media, album, artist
+from helper import CONF
+
+
+class State:
+    def __init__(self, user_id: int, quality: str):
+        match quality:
+            case "master":
+                q = session.Quality.master
+            case "lossless":
+                q = session.Quality.lossless
+            case "high":
+                q = session.Quality.high
+            case "low":
+                q = session.Quality.low
+            case _:
+                raise Exception("Quality misconfigured in conf.toml")
+        config = session.Config(quality=q)
+        self.user_id = user_id
+        self.session = session.Session(config)
+        self.favorites = user.Favorites(self.session, CONF["user_id"])
+        self._state = {
+            "albums": {},
+            "artists": {},
+            "playlists": {},
+            "tracks": {},
+        }
+
+    def login(self, auth_file: str | None = CONF["state_dir"] + "auth.json") -> None:
+        s = self.session
+        try:
+            assert auth_file
+            with open(auth_file, "r") as f:
+                a = json.load(f)
+                s.load_oauth_session(
+                    a["token_type"],
+                    a["access_token"],
+                    a["refresh_token"],
+                    datetime.fromtimestamp(a["expiry_time"]),
+                )
+        except (OSError, IndexError, AssertionError):
+            s.login_oauth_simple()
+            if (
+                s.token_type
+                and s.access_token
+                and s.refresh_token
+                and s.expiry_time
+                and auth_file
+            ):
+                data = {
+                    "token_type": s.token_type,
+                    "access_token": s.access_token,
+                    "refresh_token": s.refresh_token,
+                    "expiry_time": s.expiry_time.timestamp(),
+                }
+                with open(auth_file, "w") as f:
+                    json.dump(data, f)
+
+        assert self.session.check_login()
+
+    def set_dl_state(
+        self,
+        obj: playlist.Playlist | media.Track | album.Album | artist.Artist,
+        downloaded: bool,
+    ) -> None:
+        match type(obj):
+            case album.Album:
+                t = "albums"
+            case artist.Artist:
+                t = "artists"
+            case playlist.Playlist:
+                t = "playlists"
+            case media.Track:
+                t = "tracks"
+            case _:
+                raise Exception("Incorrect object type received")
+
+        self._state[t][obj.id] = downloaded
+
+    def write_state(
+        self, state_file_path: str = CONF["state_dir"] + "state.json"
+    ) -> None:
+        with open(state_file_path, "w") as f:
+            json.dump(self._state, f)
+
+    def load_state(
+        self, state_file_path: str = CONF["state_dir"] + "state.json"
+    ) -> None:
+        with open(state_file_path, "r") as f:
+            self._state = json.load(f)