text: import glob
  import os
  import sys
  import urllib.request
  
  import django
  import requests
  
  django.setup()
  
  from museum_site.models import *
  from museum_site.common import *
  from museum_site.core.detail_identifiers import *
  
  
  TEST_ARCHIVE_LINKS = True if "iatest" in sys.argv else False
  IGNORE_LIST = (
  
  )
  
  
  def main():
      files = File.objects.all().order_by("-id")
      for f in files:
          if f.id in IGNORE_LIST:
              continue
  
          issues = field_check(f)
  
          if len(issues["warnings"]) != 0 or len(issues["errors"]) != 0:
              print("<hr><b><a href='{}'>{}</a> - <a href='{}'>Edit</a> - <a href='{}'>Tools</a></b>\n".format(
                  f.url(), f, f.admin_url(), f.tool_url())
              )
              print("<ul>\n")
              for w in issues["warnings"]:
                  print("<li class='warning'>{}</li>\n".format(w))
              for e in issues["errors"]:
                  print("<li class='error'>{}</li>\n".format(e))
              print("</ul>\n")
  
      # Find zips without file entries
      print("<hr>\n<ul>")
      all_files = glob.glob(os.path.join(SITE_ROOT, "zgames", "*", "**"))
      for f in all_files:
          name = os.path.basename(f)
          if "/zgames/mass" in f or "/zgames/uploaded" in f:
              continue
          start = len(os.path.join(SITE_ROOT, "zgames")) + 1
          letter = f[start:start+1]
          #print(letter, name)
          match = File.objects.filter(letter=letter, filename=name).count()
          if not match:
              print("<li>ORPHANED ZIP", f, "</li>")
      print("</ul>")
  
      return True
  
  
  def field_check(f):
      exists = True
      issues = {"warnings": [], "errors": []}
  
      # Validate letter
      if f.letter not in "1abcdefghijklmnopqrstuvwxyz":
          issues["errors"].append("Invalid letter '{}'".format(f.letter))
  
      # Validate filename exists
      if not os.path.isfile(f.phys_path()):
          exists = False
          issues["errors"].append("FILE DOES NOT EXIST AT '{}'".format(f.phys_path()))
  
      if not f.sort_title:
          issues["warnings"].append("Sort title not set.")
  
      """
      if exists and f.size != os.path.getsize(f.phys_path()):
          issues["warnings"].append("File size doesn't match physical file: {}/{}".format(f.size, os.path.getsize(f.phys_path())))
      """
  
      if "," in f.author:
          issues["warnings"].append("Comma in author. Should be slash?")
  
      if "," in f.genre:
          issues["warnings"].append("Comma in genre. Should be slash.")
  
      if f.release_date and f.release_date.year < 1991:
          issues["warnings"].append("Release date is prior to 1991.")
  
      if f.release_date and f.release_source == "":
          issues["warnings"].append("Release source is blank, but release date is set.")
  
      if f.screenshot == "":
          issues["warnings"].append("No screenshot.")
  
      if f.screenshot and (not os.path.isfile(os.path.join(STATIC_PATH, f.screenshot_url()))):
          issues["errors"].append("Screenshot does not exist at {}".format(f.screenshot_url()))
  
      if f.company == None:
          issues["warnings"].append("Company is null. Use empty string for files not published under a company.")
  
      # Review related
      reviews = Review.objects.filter(zfile_id=f.id)
      rev_len = len(reviews)
      if rev_len != f.review_count:
          issues["warnings"].append("Reviews in DB do not match 'review_count': {}/{}".format(rev_len, f.review_count))
  
      # Detail related
      details = f.details.all()
      detail_list = []
      for detail in details:
          detail_list.append(detail.id)
  
      # Confirm LOST does not exist
      if DETAIL_LOST in detail_list and exists:
          issues["warnings"].append("File is marked as 'Lost', but a Zip exists.")
  
  
      articles = f.articles.all()
      article_len = len(articles)
      if article_len != f.article_count:
          issues["warnings"].append("Articles in DB do not match 'article_count': {}/{}".format(article_len, f.article_count))
  
      if not f.checksum:
          issues["warnings"].append("Checksum not set.")
  
      # Calculate file's checksum
      md5 = None
      try:
          resp = subprocess.run(["md5sum", f.phys_path()], stdout=subprocess.PIPE)
          md5 = resp.stdout[:32].decode("utf-8")
      except:
          pass
  
      if md5 and f.checksum != md5:
          issues["warnings"].append("Checksum in DB does not match calculated checksum:<br><div class='mono'>{}</div><div class='mono'>{}</div>".format(f.checksum, md5))
  
  
      # Board counts
      if (DETAIL_ZZT in detail_list or DETAIL_SZZT in detail_list) and f.playable_boards is None:
          issues["warnings"].append("File has no playable boards value but is marked as ZZT/Super ZZT")
  
      if (DETAIL_ZZT in detail_list or DETAIL_SZZT in detail_list) and f.total_boards is None:
          issues["warnings"].append("File has no total boards value but is marked as ZZT/Super ZZT")
  
      if f.archive_name == "" and (DETAIL_LOST not in detail_list and DETAIL_UPLOADED not in detail_list):
          issues["warnings"].append("File has no archive.org mirror")
  
      # Test functioning archive.org link
      if f.archive_name and TEST_ARCHIVE_LINKS:
          url = "https://archive.org/embed/" + f.archive_name
          resp = requests.get(url)
          if resp.status_code != 200:
              issues["warnings"].append("{} returned status {}".format(url, resp.status_code))
  
      # Zip related
      return issues
  
  
  if __name__ == "__main__":
      main()
  