msg_apply_prefix = f'Applying migration {version}: '
             for path in [p for p in sorted_paths if _SUFFIX_SQL == p.suffix]:
                 print(f'{msg_apply_prefix}{path}')
-                sql = SqlText(path.read_text(encoding='utf8'))
-                conn.exec(sql)
+                conn.exec_script(path)
             for path in [p for p in sorted_paths if _SUFFIX_PY == p.suffix]:
                 spec = spec_from_file_location(str(path), path)
                 assert spec is not None
 
                       (str(f.rel_path),))
     for file in VideoFile.get_all(conn):
         print(f'Calculating digest for: {file.rel_path}')
-        with open(file.full_path, 'rb') as x:
+        with open(file.full_path, 'rb') as f:
             file.sha512_digest = HashStr(
-                    file_digest(x, 'sha512').hexdigest())
+                    file_digest(f, 'sha512').hexdigest())
         file.save(conn)
 
--- /dev/null
+CREATE TEMPORARY TABLE files_backup (
+  rel_path TEXT PRIMARY KEY,
+  yt_id TEXT NOT NULL DEFAULT "",
+  flags INTEGER NOT NULL DEFAULT 0,
+  last_update TEXT NOT NULL DEFAULT "2000-01-01 12:00:00.123456",
+  sha512_digest TEXT NOT NULL,
+  FOREIGN KEY (yt_id) REFERENCES yt_videos(id)
+);
+INSERT INTO files_backup SELECT
+  rel_path,
+  yt_id,
+  flags,
+  last_update,
+  sha512_digest
+FROM files;
+DROP TABLE files;
+CREATE TABLE files (
+  sha512_digest TEXT PRIMARY KEY,
+  rel_path TEXT NOT NULL,
+  flags INTEGER NOT NULL DEFAULT 0,
+  yt_id TEXT,
+  last_update TEXT NOT NULL,
+  FOREIGN KEY (yt_id) REFERENCES yt_videos(id)
+);
+INSERT INTO files SELECT
+  sha512_digest,
+  rel_path,
+  flags,
+  yt_id,
+  last_update
+FROM files_backup;
+DROP TABLE files_backup;
 
+++ /dev/null
-CREATE TABLE yt_queries (
-  id TEXT PRIMARY KEY,
-  text TEXT NOT NULL,
-  retrieved_at TEXT NOT NULL
-);
-CREATE TABLE yt_videos (
-  id TEXT PRIMARY KEY,
-  title TEXT NOT NULL,
-  description TEXT NOT NULL,
-  published_at TEXT NOT NULL,
-  duration TEXT NOT NULL,
-  definition TEXT NOT NULL
-);
-CREATE TABLE yt_query_results (
-  query_id TEXT NOT NULL,
-  video_id TEXT NOT NULL,
-  PRIMARY KEY (query_id, video_id),
-  FOREIGN KEY (query_id) REFERENCES yt_queries(id),
-  FOREIGN KEY (video_id) REFERENCES yt_videos(id)
-);
-CREATE TABLE quota_costs (
-  id TEXT PRIMARY KEY,
-  timestamp TEXT NOT NULL,
-  cost INT NOT NULL
-);
-CREATE TABLE files (
-  rel_path TEXT PRIMARY KEY,
-  yt_id TEXT NOT NULL DEFAULT "",
-  flags INTEGER NOT NULL DEFAULT 0,
-  last_update TEXT NOT NULL DEFAULT "2000-01-01 12:00:00.123456",
-  sha512_digest TEXT NOT NULL DEFAULT "",
-  FOREIGN KEY (yt_id) REFERENCES yt_videos(id)
-);
 
--- /dev/null
+CREATE TABLE yt_queries (
+  id TEXT PRIMARY KEY,
+  text TEXT NOT NULL,
+  retrieved_at TEXT NOT NULL
+);
+CREATE TABLE yt_videos (
+  id TEXT PRIMARY KEY,
+  title TEXT NOT NULL,
+  description TEXT NOT NULL,
+  published_at TEXT NOT NULL,
+  duration TEXT NOT NULL,
+  definition TEXT NOT NULL
+);
+CREATE TABLE yt_query_results (
+  query_id TEXT NOT NULL,
+  video_id TEXT NOT NULL,
+  PRIMARY KEY (query_id, video_id),
+  FOREIGN KEY (query_id) REFERENCES yt_queries(id),
+  FOREIGN KEY (video_id) REFERENCES yt_videos(id)
+);
+CREATE TABLE quota_costs (
+  id TEXT PRIMARY KEY,
+  timestamp TEXT NOT NULL,
+  cost INT NOT NULL
+);
+CREATE TABLE files (
+  sha512_digest TEXT PRIMARY KEY,
+  rel_path TEXT NOT NULL,
+  flags INTEGER NOT NULL DEFAULT 0,
+  yt_id TEXT,
+  last_update TEXT NOT NULL,
+  FOREIGN KEY (yt_id) REFERENCES yt_videos(id)
+);
 
 <tr><th>YouTube ID:</th><td><a href="/{{page_names.yt_result}}/{{file.yt_id}}">{{file.yt_id}}</a></tr>
 <tr><th>present:</th><td>{% if file.present %}<a href="/{{page_names.download}}/{{file.yt_id}}">yes</a>{% else %}no{% endif %}</td></tr>
 </table>
-<form action="/{{page_names.file}}/{{file.rel_path_b64}}" method="POST" />
+<form action="/{{page_names.file}}/{{file.sha512_digest}}" method="POST" />
 {% for flag_name in flag_names %}
 {{ flag_name }}: <input type="checkbox" name="{{flag_name}}" {% if file.is_flag_set(flag_name) %}checked {% endif %} /><br />
 {% endfor %}
 
 {% for file in files %}
 <tr>
 <td>{{ file.size | round(3) }}</td>
-<td><input type="submit" name="play_{{file.rel_path_b64}}" value="play" {% if not file.present %}disabled {% endif %}/></td>
-<td><a href="/{{page_names.file}}/{{file.rel_path_b64}}">{{file.rel_path}}</a></td>
+<td><input type="submit" name="play_{{file.sha512_digest}}" value="play" {% if not file.present %}disabled {% endif %}/></td>
+<td><a href="/{{page_names.file}}/{{file.sha512_digest}}">{{file.rel_path}}</a></td>
 </tr>
 {% endfor %}
 </table>
 
 <input type="submit" name="up_{{idx}}" value="{% if reverse %}v{% else %}^{% endif %}" />
 <input type="submit" name="down_{{idx}}" value="{% if reverse %}^{% else %}v{% endif %}" />
 </td>
-<td><a href="/{{page_names.file}}/{{file.rel_path_b64}}">{{ file.rel_path }}</a></td>
+<td><a href="/{{page_names.file}}/{{file.sha512_digest}}">{{ file.rel_path }}</a></td>
 </tr>
 {% endfor %}
 </table>
 <table>
 <tr><td id="status" colspan=2>
 {% if running %}{% if pause %}PAUSED{% else %}PLAYING{% endif %}{% else %}STOPPED{% endif %}:<br />
-<a href="/{{page_names.file}}/{{current_video.rel_path_b64}}">{{ current_video.rel_path }}</a><br />
+<a href="/{{page_names.file}}/{{current_video.sha512_digest}}">{{ current_video.rel_path }}</a><br />
 <form action="/{{page_names.playlist}}" method="POST">
 <input type="submit" name="pause" autofocus value="{% if paused %}resume{% else %}pause{% endif %}">
 <input type="submit" name="prev" value="prev">
 
 from jinja2 import (  # type: ignore
         Environment as JinjaEnv, FileSystemLoader as JinjaFSLoader)
 from ytplom.misc import (
-        B64Str, FilesWithIndex, FlagName, NotFoundException, PlayerUpdateId,
+        HashStr, FilesWithIndex, FlagName, NotFoundException, PlayerUpdateId,
         QueryId, QueryText, QuotaCost, UrlStr, YoutubeId,
         FILE_FLAGS, PATH_APP_DATA, PATH_THUMBNAILS, YOUTUBE_URL_PREFIX,
         ensure_expected_dirs,
         if PAGE_NAMES['files'] == page_name:
             self._receive_files_command(list(postvars.keys())[0])
         elif PAGE_NAMES['file'] == page_name:
-            self._receive_video_flag(B64Str(toks_url[2]),
+            self._receive_video_flag(HashStr(toks_url[2]),
                                      [FlagName(k) for k in postvars])
         elif PAGE_NAMES['yt_queries'] == page_name:
             self._receive_yt_query(QueryText(postvars['query'][0]))
     def _receive_files_command(self, command: str) -> None:
         if command.startswith('play_'):
             with DbConn() as conn:
-                file = VideoFile.get_by_b64(conn,
-                                            B64Str(command.split('_', 1)[1]))
+                file = VideoFile.get_one(conn,
+                                         HashStr(command.split('_', 1)[1]))
             self.server.player.inject_and_play(file)
         self._redirect(Path('/'))
 
     def _receive_video_flag(self,
-                            rel_path_b64: B64Str,
+                            sha512_digest: HashStr,
                             flag_names: list[FlagName]
                             ) -> None:
         with DbConn() as conn:
-            file = VideoFile.get_by_b64(conn, rel_path_b64)
+            file = VideoFile.get_one(conn, sha512_digest)
             file.set_flags([FILE_FLAGS[name] for name in flag_names])
             file.save(conn)
             conn.commit()
         file.ensure_absence_if_deleted()
         self._redirect(Path('/')
                        .joinpath(PAGE_NAMES['file'])
-                       .joinpath(rel_path_b64))
+                       .joinpath(sha512_digest))
 
     def _receive_yt_query(self, query_txt: QueryText) -> None:
         with DbConn() as conn:
                 show_absent = params.get('show_absent', [False])[0]
                 self._send_files_index(filter_, bool(show_absent))
             elif PAGE_NAMES['file'] == page_name:
-                self._send_file_data(B64Str(toks_url[2]))
+                self._send_file_data(HashStr(toks_url[2]))
             elif PAGE_NAMES['yt_result'] == page_name:
                 self._send_yt_result(YoutubeId(toks_url[2]))
             elif PAGE_NAMES['missing'] == page_name:
                  'youtube_prefix': YOUTUBE_URL_PREFIX,
                  'queries': linked_queries})
 
-    def _send_file_data(self, rel_path_b64: B64Str) -> None:
+    def _send_file_data(self, sha512_digest: HashStr) -> None:
         with DbConn() as conn:
-            file = VideoFile.get_by_b64(conn, rel_path_b64)
+            file = VideoFile.get_one(conn, sha512_digest)
         self._send_rendered_template(
                 _NAME_TEMPLATE_FILE_DATA,
                 {'file': file, 'flag_names': list(FILE_FLAGS)})
 
 # included libs
 from typing import Any, Literal, NewType, Optional, Self, TypeAlias
 from os import chdir, environ
-from base64 import urlsafe_b64encode, urlsafe_b64decode
 from hashlib import file_digest
 from random import shuffle
 from time import time, sleep
 HashStr = NewType('HashStr', str)
 AmountDownloads = NewType('AmountDownloads', int)
 PlayerUpdateId = NewType('PlayerUpdateId', str)
-B64Str = NewType('B64Str', str)
 UrlStr = NewType('UrlStr', str)
 FilesWithIndex: TypeAlias = list[tuple[int, 'VideoFile']]
 
 QUOTA_COST_YOUTUBE_DETAILS = QuotaCost(1)
 
 # database stuff
-EXPECTED_DB_VERSION = 2
+EXPECTED_DB_VERSION = 3
 SQL_DB_VERSION = SqlText('PRAGMA user_version')
 PATH_MIGRATIONS = PATH_APP_DATA.joinpath('migrations')
 PATH_DB_SCHEMA = PATH_MIGRATIONS.joinpath(f'init_{EXPECTED_DB_VERSION}.sql')
         """Wrapper around sqlite3.Connection.execute."""
         return self._conn.execute(sql, inputs)
 
+    def exec_script(self, path: Path) -> None:
+        """Simplified sqlite3.Connection.executescript."""
+        self._conn.executescript(path.read_text(encoding='utf8'))
+
     def commit(self) -> None:
         """Commit changes (i.e. DbData.save() calls) to database."""
         self._conn.commit()
 
 class VideoFile(DbData):
     """Collects data about downloaded files."""
-    id_name = 'rel_path'
+    id_name = 'sha512_digest'
     _table_name = 'files'
-    _cols = ('rel_path', 'yt_id', 'flags', 'last_update', 'sha512_digest')
+    _cols = ('sha512_digest', 'rel_path', 'flags', 'yt_id', 'last_update')
     last_update: DatetimeStr
     rel_path: Path
 
     def __init__(self,
                  rel_path: Path,
-                 yt_id: YoutubeId,
+                 yt_id: Optional[YoutubeId] = None,
                  flags: FlagsInt = FlagsInt(0),
                  last_update: Optional[DatetimeStr] = None,
                  sha512_digest: Optional[HashStr] = None
             raise NotFoundException(f'no entry for file to Youtube ID {yt_id}')
         return cls._from_table_row(row)
 
-    @classmethod
-    def get_by_b64(cls, conn: DbConn, rel_path_b64: B64Str) -> Self:
-        """Retrieve by .rel_path provided as urlsafe_b64 encoding."""
-        return cls.get_one(conn, urlsafe_b64decode(rel_path_b64).decode())
-
-    @property
-    def rel_path_b64(self) -> B64Str:
-        """Return .rel_path as urlsafe_b64 e3ncoding."""
-        return B64Str(urlsafe_b64encode(str(self.rel_path).encode()).decode())
-
     @property
     def full_path(self) -> Path:
         """Return self.rel_path suffixed under PATH_DOWNLOADS."""