seen from Israel

seen from Türkiye

seen from Belgium
seen from United States
seen from Malaysia
seen from France
seen from T1
seen from Türkiye
seen from United States
seen from China
seen from China
seen from Ecuador

seen from Australia
seen from United States
seen from United States

seen from United States

seen from Italy
seen from China

seen from Malaysia
seen from United Kingdom
Feel Free To Add - Tumblr Alteratives Or Exras
So with tumblr kind of likely to kill itself, here’s other places to look or at least add to your Tumblr Experience.. Please feel free to add to this list.
Dreamhost.com - Web hosting, but has a cheap Wordpress-only package.
Dreamwidth.org - A post-LJ journaling system.
Newgrounds - Newgrounds is actively soliciting Tumblr users.
Pillowfort.io - As of this writing still getting up to speed, but they have a clear vision.
Soup.io - A Tumblr alternative, which will also import your Tumblrs.
Typepad
wordpress.com - Creates a wordpress site, with free and paid options.
ZOOMING VARIATION
And internet history repeats..
Yeah, they said they sent out emails, and posted it on their own blog, but I got it, and I looked through all my email accounts, including spam folders. With no warning, TypePad closed up. I found out by trying to post to it, as usual, and got some error message. I clicked on link to my blog page, and got this huge CLOSED graphic, with lame-assed excuses as to why we could no longer even archive…
Typepad Poof! Gone. No warning, nothing. I just headed there and this was what I saw.
Typepad
Is shutting down. Shame. But perhaps it was inevitable.
Typepad is Shutting down - How to migrate posts to Tumblr
The blogging service Typepad has announced it is shutting down and deleting the data at the end of Sept 2025. In order to not lose my posts from 20-15 years ago I exported the posts from Typepad, which are saved in one txt file in Typepad's MovableType format. However since this is an older and less well known format there is a lack of support for importing the posts into Tumblr.
So I worked with ChatGPT and developed a python script that reads the exported posts and posts them to Tumblr and preserves the date they were originally posted. The script works with Tumblr's OAuth support for apps to post your exported Typepad blog posts to your Tumblr blog.
Note: There is a limit in Tumblr of about 250 posts per Day to prevent spam. So if you have more than that you can use the --start-index and --limit parameters to batch posts each day.
Here is the typepad_to_tymblr.py script that worked for me to help you get started with posting your Typepad export to Tumblr.
You can copy the text from the above and paste it into ChatGPT or Claude and work through the script if you need help troubleshooting how to run it and adding an app with OAuth token.
For the initial setup you can use the --dry-run parameter to check if your python environment and the python script can run properly before it attempts to post to Tumblr.
python3 typepad_to_tumblr.py \ -f /path/to/TypepadExport.txt \ -b yourblog.tumblr.com \ --ck YOUR_CONSUMER_KEY \ --cs YOUR_CONSUMER_SECRET \ --assume-tz Asia/Tokyo \ --start-index 10 \ --dry-run \ --limit 5
For example, the below parameters would post from the 10th blog entry in your Typepad export and post the next 5 entries: python3 typepad_to_tumblr.py \ -f /path/to/TypepadExport.txt \ -b yourblog.tumblr.com \ --ck YOUR_CONSUMER_KEY \ --cs YOUR_CONSUMER_SECRET \ --assume-tz Asia/Tokyo \ --start-index 10 \ --limit 5
Next you would start with the 15th blog entry in your export and post the next 5 entries, so you can change the --start-index and --limit values to fit your needs: python3 typepad_to_tumblr.py \ -f /path/to/TypepadExport.txt \ -b yourblog.tumblr.com \ --ck YOUR_CONSUMER_KEY \ --cs YOUR_CONSUMER_SECRET \ --assume-tz Asia/Tokyo \ --start-index 15 \ --limit 5
typepad_to_tumblr.py
#!/usr/bin/env python3
""" Import Typepad/Movable Type export (.txt) into Tumblr as Text posts, preserving original timestamps where possible.
v4 highlights:
Correct entry/section delimiter handling:
'--------' (8+ dashes) => entry boundary
'-----' (5+ dashes) => section boundary
Case-insensitive headers; scans every line in a section (so DATE: on line 2, after UNIQUE URL:, is captured)
Robust date parsing with --assume-tz (python-dateutil)
Fallbacks: BODY date, UNIQUE URL (/YYYY/MM/DD/ or /YYYY/MM/), --default-date
Dry-run, batching, resume; OAuth 1.0a flow with local token storage
Optional --log-unparsed to review items that lacked a header date """
import argparse import os import re import sys import time import webbrowser import html from dataclasses import dataclass from typing import List, Optional, Dict, Tuple from datetime import datetime from email.utils import format_datetime
import requests from requests_oauthlib import OAuth1, OAuth1Session from dateutil import tz from dateutil import parser as dparser
TUMBLR_API_BASE = "https://api.tumblr.com/v2" REQUEST_TOKEN_URL = "https://www.tumblr.com/oauth/request_token" AUTHORIZE_URL = "https://www.tumblr.com/oauth/authorize" ACCESS_TOKEN_URL = "https://www.tumblr.com/oauth/access_token"
#---------- Typepad (Movable Type) parsing ----------
@dataclass class MTEntry: title: str date: Optional[datetime] body_html: str tags: List[str] unique_url: str had_header_date: bool # whether a header DATE-like field was present & parsed
ENTRY_DELIM = re.compile(r'^\s-{8,}\s$', re.MULTILINE) # '--------' => entry boundary SECTION_DELIM = re.compile(r'^\s-{5,}\s$', re.MULTILINE) # '-----' => section boundary
def smart_parse_date(s: str, default_tz: str = "UTC") -> Optional[datetime]: """Parse a wide variety of date strings. Return UTC-aware datetime or None.""" s = (s or "").strip() if not s: return None try: dt = dparser.parse(s, fuzzy=True) if dt.tzinfo is None: tzinfo = tz.gettz(default_tz) or tz.UTC dt = dt.replace(tzinfo=tzinfo) return dt.astimezone(tz.UTC) except Exception: return None
def date_from_body_html(body_html: str, default_tz: str = "UTC") -> Optional[datetime]: """Try to find a date embedded in BODY/EXTENDED BODY HTML.""" if not body_html: return None text = html.unescape(body_html) # Common pattern:
… m = re.search(r']class=["\']?date["\']?[^>]>(.?)', text, re.I | re.S) if m: dt = smart_parse_date(m.group(1), default_tz) if dt: return dt # Fallback: a “Month dd, yyyy …” snippet m = re.search(r'([A-Za-z]{3,9}\s+\d{1,2},\s+\d{4}[^<\n])', text) if m: return smart_parse_date(m.group(1), default_tz) return None
def date_from_unique_url(url: str, default_tz: str = "UTC") -> Optional[datetime]: """ Infer a date from the permalink path: …/YYYY/MM/DD/slug -> that exact day @ 09:00 local …/YYYY/MM/slug -> month start @ 09:00 local """ if not url: return None tzinfo = tz.gettz(default_tz) or tz.UTC # Try YYYY/MM/DD m = re.search(r'/(\d{4})/(\d{2})/(\d{2})/', url) if m: y, mo, d = map(int, m.groups()) local_dt = datetime(y, mo, d, 9, 0, 0, tzinfo=tzinfo) return local_dt.astimezone(tz.UTC) # Try YYYY/MM m = re.search(r'/(\d{4})/(\d{2})/', url) if m: y, mo = map(int, m.groups()) local_dt = datetime(y, mo, 1, 9, 0, 0, tzinfo=tzinfo) return local_dt.astimezone(tz.UTC) return None
def parse_single_entry(entry_text: str, assume_tz: str) -> Optional[MTEntry]: """ Parse one entry block (between '--------' lines), aggregating its sections split by '-----'. This version scans every line in a section, so secondary header lines are handled. """ sections = [s.strip("\n") for s in SECTION_DELIM.split(entry_text)] fields: Dict[str, str] = {} body_lines: List[str] = [] ext_lines: List[str] = [] in_body = in_ext = Falsedef end_sections(): nonlocal in_body, in_ext in_body = False in_ext = False def val_after_colon(s: str) -> str: p = s.split(":", 1) return p[1].strip() if len(p) > 1 else "" for sec in sections: lines = sec.splitlines() for ln in lines: head = ln.strip() up = head.upper() # Header keys (case-insensitive) spread across lines if up.startswith("TITLE:"): end_sections() fields["TITLE"] = val_after_colon(head) elif (up.startswith("DATE:") or up.startswith("PUBLISH DATE:") or up.startswith("CREATED ON:") or up.startswith("UPDATED ON:")): end_sections() value = val_after_colon(head) if value and not fields.get("DATE"): fields["DATE"] = value elif up.startswith("TAGS:"): end_sections() fields["TAGS"] = val_after_colon(head) elif up.startswith("CATEGORY:"): end_sections() fields["CATEGORIES"] = (fields.get("CATEGORIES", "") + "," + val_after_colon(head)).strip(",") elif up.startswith("UNIQUE URL:"): end_sections() fields["UNIQUE_URL"] = val_after_colon(head) elif up.startswith("BODY:"): end_sections() in_body = True elif up.startswith("EXTENDED BODY:"): end_sections() in_ext = True else: # Content lines belong to the current BODY/EXTENDED section if in_body: body_lines.append(ln) elif in_ext: ext_lines.append(ln) # otherwise ignore metadata noise title = (fields.get("TITLE") or "").strip() # Tags: combine TAGS + CATEGORIES raw_tags = ",".join(filter(None, [fields.get("TAGS", ""), fields.get("CATEGORIES", "")])) tags = [t.strip() for t in re.split(r'[;,]', raw_tags) if t.strip()] # Body HTML body_html = "\n".join(body_lines).strip() ext_html = "\n".join(ext_lines).strip() if ext_html: body_html = body_html + ("\n\n<hr/>\n\n" if body_html else "") + ext_html if not body_html: body_html = "" # Date (robust with fallbacks) header_date_str = (fields.get("DATE") or "").strip() parsed = smart_parse_date(header_date_str, default_tz=assume_tz) had_header_date = parsed is not None if parsed is None: candidate = date_from_body_html(body_html, default_tz=assume_tz) if candidate: parsed = candidate if parsed is None: parsed = date_from_unique_url(fields.get("UNIQUE_URL", ""), default_tz=assume_tz) unique_url = fields.get("UNIQUE_URL", "") return MTEntry( title=(title if title else ((body_html[:60] + "…") if len(body_html) > 60 else (body_html or "(untitled)"))), date=parsed, # may still be None for truly undated items body_html=body_html, tags=tags, unique_url=unique_url, had_header_date=had_header_date )
def parse_mt_export(path: str, assume_tz: str = "UTC") -> List[MTEntry]: """Read the export, split entries on '--------', then parse each entry.""" with open(path, 'r', encoding='utf-8', errors='replace') as f: text = f.read() raw_entries = [blk for blk in ENTRY_DELIM.split(text) if blk.strip()] entries: List[MTEntry] = [] for blk in raw_entries: e = parse_single_entry(blk, assume_tz=assume_tz) if e: entries.append(e) return entries
#---------- Tumblr OAuth + posting ----------
def oauth1_authorize(consumer_key: str, consumer_secret: str) -> Tuple[str, str]: """3-legged OAuth 1.0a flow; opens browser; user pastes verifier.""" oauth = OAuth1Session(consumer_key, client_secret=consumer_secret, callback_uri="http://localhost/callback") fetch_resp = oauth.fetch_request_token(REQUEST_TOKEN_URL) resource_owner_key = fetch_resp.get('oauth_token') resource_owner_secret = fetch_resp.get('oauth_token_secret')auth_url = oauth.authorization_url(AUTHORIZE_URL) print("\nA browser window should open for Tumblr authorization.") print("If it does not, open this URL manually:\n", auth_url, "\n") try: webbrowser.open(auth_url) except Exception: pass verifier = input("After approving, Tumblr shows a verification code (oauth_verifier).\nPaste it here: ").strip() oauth = OAuth1Session( consumer_key, client_secret=consumer_secret, resource_owner_key=resource_owner_key, resource_owner_secret=resource_owner_secret, verifier=verifier, ) tokens = oauth.fetch_access_token(ACCESS_TOKEN_URL) return tokens['oauth_token'], tokens['oauth_token_secret']
def post_text( blog_identifier: str, consumer_key: str, consumer_secret: str, oauth_token: str, oauth_token_secret: str, title: str, body_html: str, tags: List[str], dt_utc: datetime, dry_run: bool = False ) -> Dict: """Create a Tumblr text post, preserving publish date.""" url = f"{TUMBLR_API_BASE}/blog/{blog_identifier}/post" if dt_utc.tzinfo is None: dt_utc = dt_utc.replace(tzinfo=tz.UTC) date_rfc2822 = format_datetime(dt_utc)payload = { "type": "text", "state": "published", "title": title, "body": body_html, "tags": ",".join(tags) if tags else "", "date": date_rfc2822, } if dry_run: print("[dry-run] Would POST:", {"url": url, "payload": {k: (v if k != "body" else f"<{len(v)} chars>") for k, v in payload.items()}}) return {"dry_run": True, "ok": True, "id": None} auth = OAuth1(consumer_key, client_secret=consumer_secret, resource_owner_key=oauth_token, resource_owner_secret=oauth_token_secret) resp = requests.post(url, data=payload, auth=auth, timeout=30) try: data = resp.json() except Exception: data = {"meta": {"status": resp.status_code}, "raw": resp.text} status = data.get("meta", {}).get("status", resp.status_code) if status not in (201, 202): raise RuntimeError(f"Tumblr error: HTTP {resp.status_code} {data}") return data
def save_tokens(path: str, token: str, secret: str): with open(path, "w", encoding="utf-8") as f: f.write(token.strip() + "\n" + secret.strip() + "\n") os.chmod(path, 0o600)
def load_tokens(path: str) -> Optional[Tuple[str, str]]: if not os.path.exists(path): return None with open(path, "r", encoding="utf-8") as f: lines = [l.strip() for l in f.read().splitlines() if l.strip()] if len(lines) >= 2: return lines[0], lines[1] return None
def parse_iso_or_none(s: Optional[str], tzname: str) -> Optional[datetime]: if not s: return None try: dt = dparser.parse(s) if dt.tzinfo is None: dt = dt.replace(tzinfo=tz.gettz(tzname) or tz.UTC) return dt.astimezone(tz.UTC) except Exception: return None
def main(): ap = argparse.ArgumentParser(description="Import Typepad/Movable Type export to Tumblr (as Text posts).") ap.add_argument("-f", "--file", required=True, help="Path to Typepad/Movable Type export file (.txt)") ap.add_argument("-b", "--blog", required=True, help="Tumblr blog identifier, e.g. myblog.tumblr.com") ap.add_argument("--ck", "--consumer-key", dest="consumer_key", required=True, help="Tumblr Consumer Key") ap.add_argument("--cs", "--consumer-secret", dest="consumer_secret", required=True, help="Tumblr Consumer Secret") ap.add_argument("--tokens", default=os.path.expanduser("~/.tumblr_oauth_tokens"), help="File to store OAuth access tokens (created after first auth)") ap.add_argument("--assume-tz", default="UTC", help="Timezone for naive dates (e.g., 'Asia/Tokyo').") ap.add_argument("--default-date", help="Fallback date if none can be parsed (e.g., '2001-01-01 09:00'). Uses --assume-tz if naive.") ap.add_argument("--start-index", type=int, default=0, help="Start at this entry index (for resuming)") ap.add_argument("--limit", type=int, default=0, help="Post at most N entries (0 = no limit)") ap.add_argument("--sleep", type=float, default=2.0, help="Seconds to sleep between posts (respect rate limits)") ap.add_argument("--dry-run", action="store_true", help="Parse & show payloads without posting") ap.add_argument("--log-unparsed", help="Write entries that lacked a header date to this TSV file") args = ap.parse_args()# OAuth: load or obtain tokens toks = load_tokens(args.tokens) if toks is None and not args.dry_run: print("No OAuth tokens found; starting authorization flow…") atok, asec = oauth1_authorize(args.consumer_key, args.consumer_secret) save_tokens(args.tokens, atok, asec) toks = (atok, asec) print(f"Saved access tokens to {args.tokens}") elif toks is None and args.dry_run: toks = ("", "") print("Parsing export…") entries = parse_mt_export(args.file, assume_tz=args.assume_tz) print(f"Found {len(entries)} entries.") # Diagnostics unparsed_rows = [] default_dt = parse_iso_or_none(args.default_date, args.assume_tz) if args.default_date else None start = max(0, args.start_index) end = len(entries) if args.limit == 0 else min(len(entries), start + args.limit) posted = 0 for idx in range(start, end): e = entries[idx] # Finalize date fallback policy for any truly undated entry dt = e.date if dt is None: if default_dt is not None: dt = default_dt else: dt = datetime.now(tz.UTC) print(f"[warn] Could not parse DATE '', using now for this entry. (title='{e.title}')", file=sys.stderr) if args.log_unparsed and not e.had_header_date: unparsed_rows.append(f"{idx}\t{e.title}\t{e.unique_url}") print(f"[{idx+1}/{len(entries)}] {e.title} — {dt.isoformat()} ({len(e.tags)} tags)") try: resp = post_text( blog_identifier=args.blog, consumer_key=args.consumer_key, consumer_secret=args.consumer_secret, oauth_token=toks[0], oauth_token_secret=toks[1], title=e.title, body_html=e.body_html, tags=e.tags, dt_utc=dt, dry_run=args.dry_run ) if not args.dry_run: post_id = resp.get("response", {}).get("id") print(f" -> posted OK (id={post_id})") else: print(" -> dry-run OK") posted += 1 except Exception as ex: print(f" !! error posting entry {idx}: {ex}", file=sys.stderr) if idx < end - 1 and args.sleep > 0 and not args.dry_run: time.sleep(args.sleep) if args.log_unparsed: with open(args.log_unparsed, "w", encoding="utf-8") as f: f.write("index\ttitle\tunique_url\n") for row in unparsed_rows: f.write(row + "\n") print(f"[info] Wrote {len(unparsed_rows)} rows to {args.log_unparsed}") print(f"Done. {'Dry-run ' if args.dry_run else ''}Processed {posted} entries from index {start} to {end-1}.")
if name == "main": main()