网站建设资讯

NEWS

网站建设资讯

Java微信公众平台之素材管理

微信素材管理和群发这块文档对Java很不友好。本文只对新增临时素材,新增永久素材做介绍,其余获取、删除、修改自行补充

创新互联是一家以重庆网站建设、网页设计、品牌设计、软件运维、成都网站推广、小程序App开发等移动开发为一体互联网公司。已累计为履带搅拌车等众行业中小客户提供优质的互联网建站和软件开发服务。

公众号经常有需要用到一些临时性的多媒体素材的场景,例如在使用接口特别是发送消息时,对多媒体文件、多媒体消息的获取和调用等操作,是通过media_id来进行的。素材管理接口对所有认证的订阅号和服务号开放

素材的限制

图片(image): 2M,支持PNG\JPEG\JPG\GIF格式
语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式
视频(video):10MB,支持MP4格式
缩略图(thumb):64KB,支持JPG格式

一、新增临时素材

接口:https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE,再传一个媒体文件类型,可以是图片(image)、语音(voice)、视频(video)和缩略图(thumb)。

1、订阅号和服务号要通过认证
2、临时素材media_id是可复用的

3、媒体文件在微信后台保存时间为3天,即3天后media_id失效。

/** 
 * 上传临时素材(本地) 
 * 
 * @param accessToken 
 * @param type 
 *   媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb) 
 * @param path 
 *   图片路径 
 * @return 
 */ 
 
public static UploadMediasResult uploadTempMediaFile(String accessToken, String type, String path) { 
 UploadMediasResult result = null; 
 TreeMap params = new TreeMap<>(); 
 params.put("access_token", accessToken); 
 params.put("type", type); 
 try { 
  String json = HttpsUploadMediaFile(SystemConfig.POST_METHOD, WechatConfig.UPLOAD_TEMP_MEDIA_TYPE_URL, 
    params, path); 
  result = JsonUtil.fromJsonString(json, UploadMediasResult.class); 
 } catch (Exception e) { 
  e.printStackTrace(); 
 } 
 return result; 
} 
 
/** 
 * 上传临时素材(网络) 
 * 
 * @param accessToken 
 * @param type 
 *   媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb) 
 * @param path 
 *   图片路径 
 * @return 
 */ 
public static UploadMediasResult uploadTempMedia(String accessToken, String type, String path) { 
 UploadMediasResult result = null; 
 TreeMap params = new TreeMap<>(); 
 params.put("access_token", accessToken); 
 params.put("type", type); 
 try { 
  String json = HttpsUploadMedia(SystemConfig.POST_METHOD, WechatConfig.UPLOAD_TEMP_MEDIA_TYPE_URL, params, 
    path, 0, 0); 
  result = JsonUtil.fromJsonString(json, UploadMediasResult.class); 
 } catch (Exception e) { 
  e.printStackTrace(); 
 } 
 return result; 
} 

二、新增永久素材

接口:https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=ACCESS_TOKEN&type=TYPE,媒体文件类型,分别有图片(image)、语音(voice)、视频(video,例外)和缩略图(thumb)

/** 
 * 上传永久素材(本地) 
 * 
 * @param accessToken 
 * @param type 
 *   媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb) 
 * @return 
 */ 
public static UploadMediasResult uploadForeverMediaFile(String accessToken, String type, String path) { 
 UploadMediasResult result = null; 
 TreeMap params = new TreeMap<>(); 
 params.put("access_token", accessToken); 
 params.put("type", type); 
 try { 
  String json = HttpsUploadMediaFile(SystemConfig.POST_METHOD, WechatConfig.UPLOAD_FOREVER_MEDIA_TYPE_URL, 
    params, path); 
  result = JsonUtil.fromJsonString(json, UploadMediasResult.class); 
 } catch (Exception e) { 
  e.printStackTrace(); 
 } 
 return result; 
} 
 
/** 
 * 上传永久素材(网络) 
 * 
 * @param accessToken 
 * @param type 
 *   媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb) 
 * @return 
 */ 
public static UploadMediasResult uploadForeverMedia(String accessToken, String type, String path) { 
 UploadMediasResult result = null; 
 TreeMap params = new TreeMap<>(); 
 params.put("access_token", accessToken); 
 params.put("type", type); 
 try { 
  String json = HttpsUploadMedia(SystemConfig.POST_METHOD, WechatConfig.UPLOAD_FOREVER_MEDIA_TYPE_URL, params, 
    path, 0, 0); 
  result = JsonUtil.fromJsonString(json, UploadMediasResult.class); 
 } catch (Exception e) { 
  e.printStackTrace(); 
 } 
 return result; 
} 

 新增永久视频素材需特别注意,在上传视频素材时需要POST另一个表单,id为description,包含素材的描述信息title和introduction,内容格式为JSON

/** 
 * 上传永久素材(video) 
 * 
 * @param accessToken 
 * @return 
 */ 
public static String uploadForeverMediaFile(String accessToken, String title, String introduction, String path) { 
 TreeMap params = new TreeMap<>(); 
 params.put("access_token", accessToken); 
 params.put("type", "video"); 
 String mediaId = null; 
 try { 
  String json = HttpsUploadVideoMediaFile(SystemConfig.POST_METHOD, 
    WechatConfig.UPLOAD_FOREVER_MEDIA_TYPE_URL, params, path, title, introduction); 
  mediaId = JsonUtil.fromJsonString(json, "media_id"); 
 } catch (Exception e) { 
  e.printStackTrace(); 
 } 
 return mediaId; 
} 
 
/** 
 * 上传永久素材(video,网络) 
 * 
 * @param accessToken 
 * @return 
 */ 
public static String uploadForeverMedia(String accessToken, String title, String introduction, String path) { 
 TreeMap params = new TreeMap<>(); 
 params.put("access_token", accessToken); 
 params.put("type", "video"); 
 String mediaId = null; 
 try { 
  String json = HttpsUploadVideoMedia(SystemConfig.POST_METHOD, WechatConfig.UPLOAD_FOREVER_MEDIA_TYPE_URL, 
    params, path, title, introduction, 0, 0); 
  mediaId = JsonUtil.fromJsonString(json, "media_id"); 
 } catch (Exception e) { 
  e.printStackTrace(); 
 } 
 return mediaId; 
} 

三、新增永久图文素材

接口:https://api.weixin.qq.com/cgi-bin/material/add_news?access_token=ACCESS_TOKEN,post信息参见UploadNewsMedia 实体类

 对于常用的素材,开发者可通过本接口上传到微信服务器,永久使用.

1、永久图片素材新增后,将带有URL返回给开发者,开发者可以在腾讯系域名内使用(腾讯系域名外使用,图片将被屏蔽)。
2、公众号的素材库保存总数量有上限:图文消息素材、图片素材上限为5000,其他类型为1000。
3、图文消息的具体内容中,微信后台将过滤外部的图片链接,图片url需通过"上传图文消息内的图片获取URL"接口上传图片获取。
4、"上传图文消息内的图片获取URL"接口所上传的图片,不占用公众号的素材库中图片数量的5000个的限制,图片仅支持jpg/png格式,大小必须在1MB以下。
5、图文消息支持正文中插入自己帐号和其他公众号已群发文章链接的能力。

/** 
 * 上传永久图文消息的素材 
 * 
 * @param accessToken 
 *   授权token 
 * @param entity 
 *   图文消息对象 
 * @return 
 */ 
public static UploadMediasResult uploadNewsMedia(String accessToken, List entity) { 
 UploadMediasResult result = null; 
 TreeMap params = new TreeMap<>(); 
 params.put("access_token", accessToken); 
 // post 提交的参数 
 TreeMap> dataParams = new TreeMap>(); 
 dataParams.put("articles", entity); 
 String data = JsonUtil.toJsonString(dataParams); 
 String json = HttpReqUtil.HttpsDefaultExecute(SystemConfig.POST_METHOD, 
   WechatConfig.UPLOAD_FOREVER_NEWS_MEDIA_URL, params, data); 
 result = JsonUtil.fromJsonString(json, UploadMediasResult.class); 
 return result; 
} 

四、上传图文消息内的图片获取URL

接口:https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=ACCESS_TOKEN

本接口所上传的图片不占用公众号的素材库中图片数量的5000个的限制。图片仅支持jpg/png格式,大小必须在1MB以下,此接口返回的url就是上传图片的URL,可放置图文消息中使用。

/** 
 * 上传图文消息内的图片获取URL(本地) 
 * 
 * @param accessToken 
 * @param path 
 * @return 
 */ 
public static String uploadImgMediaFile(String accessToken, String path) { 
 TreeMap params = new TreeMap<>(); 
 params.put("access_token", accessToken); 
 String url = null; 
 try { 
  String json = HttpsUploadMediaFile(SystemConfig.POST_METHOD, WechatConfig.UPLOAD_IMG_MEDIA_URL, params, 
    path); 
  url = JsonUtil.fromJsonString(json, "url"); 
 } catch (Exception e) { 
  e.printStackTrace(); 
 } 
 return url; 
} 
 
/** 
 * 上传图文消息内的图片获取URL(网络) 
 * 
 * @param accessToken 
 * @param path 
 * @return 
 */ 
public static String uploadImgMedia(String accessToken, String path) { 
 TreeMap params = new TreeMap(); 
 params.put("access_token", accessToken); 
 String url = null; 
 try { 
  String json = HttpsUploadMedia(SystemConfig.POST_METHOD, WechatConfig.UPLOAD_IMG_MEDIA_URL, params, path, 0, 
    0); 
  url = JsonUtil.fromJsonString(json, "url"); 
 } catch (Exception e) { 
  e.printStackTrace(); 
 } 
 return url; 
} 

五、部分工具类

配置类

public static final String UPLOAD_IMG_MEDIA_URL = "https://api.weixin.qq.com/cgi-bin/media/uploadimg"; 
public static final String UPLOAD_FOREVER_NEWS_MEDIA_URL = "https://api.weixin.qq.com/cgi-bin/material/add_news"; 
public static final String UPLOAD_TEMP_MEDIA_TYPE_URL = "https://api.weixin.qq.com/cgi-bin/media/upload"; 
public static final String UPLOAD_FOREVER_MEDIA_TYPE_URL = "https://api.weixin.qq.com/cgi-bin/material/add_material"; 

上传图文消息素材返回类

package com.phil.wechat.msg.model.media; 
 
/** 
 * 上传图文消息素材返回的结果 
 * @author phil 
 * @date 2017年9月20日 
 * 
 */ 
public class UploadMediasResult { 
 private String type; // 媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb),次数为news,即图文消息 
 private String media_id; // 媒体文件/图文消息上传后获取的唯一标识 
 private String created_at; // 媒体文件上传时间 
} 

上传图文消息素材实体类

package com.phil.wechat.msg.model.media; 
 
import java.io.Serializable; 
 
/** 
 * 上传图文消息素材实体类 
 * @author phil 
 * @date 2017年9月20日 
 */ 
public class UploadNewsMedia implements Serializable { 
 
 private static final long serialVersionUID = 6551817058101753854L; 
 private String thumb_media_id; // 图文消息缩略图的media_id,可以在基础支持-上传多媒体文件接口中获得 
 private String author; // 图文消息的作者 
 private String title; // 图文消息的标题 
 private String content_source_url; // 图文消息点击阅读原文的链接 
 private String content; // 图文消息页面的内容,支持HTML标签 
 private String digest; // 图文消息的描述 
 private int show_conver_pic; // 是否显示为封面 1表示显示为封面 0 不显示为封面 
} 

上传方法

/** 
 * 上传媒体文件(本地) 
 * 
 * @param method 
 *   请求方法 GET/POST 
 * @param path 
 *   api的路径 
 * @param param 
 *   api参数 
 * @param mediaPath 
 *   待上传的image/music 的path 
 * @return 
 * @throws Exception 
 */ 
public static String HttpsUploadMediaFile(String method, String path, Map param, String mediaPath) 
  throws Exception { 
 String result = null; 
 URL url = new URL(setParmas(param, path, "")); 
 OutputStream output = null; 
 DataInputStream inputStream = null; 
 try { 
  File file = new File(mediaPath); 
  if (!file.isFile() || !file.exists()) { 
   throw new IOException("file is not exist"); 
  } 
  HttpURLConnection con = (HttpURLConnection) url.openConnection(); 
  con.setDoInput(true); 
  con.setDoOutput(true); 
  con.setUseCaches(false); 
  con.setRequestMethod(SystemConfig.POST_METHOD); 
  // 设置请求头信息 
  con.setRequestProperty("Connection", "Keep-Alive"); 
  con.setRequestProperty("Charset", SystemConfig.DEFAULT_CHARACTER_ENCODING); 
  // 设置边界 
  String boundary = "----------" + System.currentTimeMillis(); 
  con.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); 
  // 请求正文信息 
  // 第一部分 
  output = new DataOutputStream(con.getOutputStream()); 
  IOUtils.write(("--" + boundary + "\r\n").getBytes(SystemConfig.DEFAULT_CHARACTER_ENCODING), output); 
  IOUtils.write(("Content-Disposition: form-data;name=\"media\"; filename=\"" + file.getName() + "\"\r\n") 
    .getBytes(SystemConfig.DEFAULT_CHARACTER_ENCODING), output); 
  IOUtils.write( 
    "Content-Type:application/octet-stream\r\n\r\n".getBytes(SystemConfig.DEFAULT_CHARACTER_ENCODING), 
    output); 
  // IOUtils.write(("Content-Type: "+ fileExt + "\r\n\r\n").getBytes(), output); 
  // 文件正文部分 
  // 把文件已流文件的方式 推入到url中 
  inputStream = new DataInputStream(new FileInputStream(file)); 
  IOUtils.copy(inputStream, output); 
  // 结尾部分 
  IOUtils.write(("\r\n--" + boundary + "--\r\n").getBytes(SystemConfig.DEFAULT_CHARACTER_ENCODING), output); 
  output.flush(); 
  result = inputStreamToString(con.getInputStream()); 
 } catch (MalformedURLException e) { 
  e.printStackTrace(); 
 } catch (ProtocolException e) { 
  e.printStackTrace(); 
 } catch (IOException e) { 
  throw new IOException("read data error"); 
 } finally { 
  IOUtils.closeQuietly(output); 
  IOUtils.closeQuietly(inputStream); 
 } 
 return result; 
} 
 
/** 
 * 上传媒体文件(不能本地) 
 * 
 * @param method 
 *   请求方法 GET/POST 
 * @param path 
 *   api的路径 
 * @param param 
 *   api参数 
 * @param mediaPath 
 *   待上传的image/music 的path 
 * @param connTime 
 *   连接时间 默认为5000 
 * @param readTime 
 *   读取时间 默认为5000 
 * @return 
 * @throws Exception 
 */ 
public static String HttpsUploadMedia(String method, String path, Map param, String mediaPath, 
  int connTime, int readTime) throws Exception { 
 String result = ""; 
 URL url = new URL(setParmas(param, path, "")); 
 OutputStream output = null; 
 BufferedInputStream inputStream = null; 
 try { 
  String boundary = "----"; 
  HttpURLConnection conn = getConnection(method, url); 
  conn.setConnectTimeout(connTime == 0 ? DEFAULT_CONNTIME : connTime); 
  conn.setReadTimeout(readTime == 0 ? DEFAULT_UPLOAD_READTIME : readTime); 
  conn.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary); 
  output = conn.getOutputStream(); 
  URL mediaUrl = new URL(mediaPath); 
  if (mediaUrl != null) { 
   HttpURLConnection mediaConn = (HttpURLConnection) mediaUrl.openConnection(); 
   mediaConn.setDoOutput(true); 
   mediaConn.setUseCaches(false); 
   mediaConn.setRequestMethod(SystemConfig.GET_METHOD); 
   mediaConn.setConnectTimeout(connTime == 0 ? DEFAULT_CONNTIME : connTime); 
   mediaConn.setReadTimeout(readTime == 0 ? DEFAULT_UPLOAD_READTIME : readTime); 
   String connType = mediaConn.getContentType(); 
   // 获得文件扩展 
   String fileExt = getFileExt(connType); 
   IOUtils.write(("--" + boundary + "\r\n").getBytes(), output); 
   IOUtils.write(("Content-Disposition: form-data; name=\"media\"; filename=\"" + getFileName(mediaPath) 
     + "\"\r\n").getBytes(), output); 
   IOUtils.write(("Content-Type: " + fileExt + "\r\n\r\n").getBytes(), output); 
   inputStream = new BufferedInputStream(mediaConn.getInputStream()); 
   IOUtils.copy(inputStream, output); 
   IOUtils.write(("\r\n----" + boundary + "--\r\n").getBytes(), output); 
   mediaConn.disconnect(); 
   // 获取输入流 
   result = inputStreamToString(conn.getInputStream()); 
  } 
 } catch (MalformedURLException e) { 
  e.printStackTrace(); 
 } catch (ProtocolException e) { 
  e.printStackTrace(); 
 } catch (IOException e) { 
  e.printStackTrace(); 
 } finally { 
  IOUtils.closeQuietly(output); 
  IOUtils.closeQuietly(inputStream); 
 } 
 return result; 
} 
 
/** 
 * 上传Video媒体文件(本地) 
 * 
 * @param method 
 *   请求方法 GET/POST 
 * @param path 
 *   api的路径 
 * @param param 
 *   api参数 
 * @param mediaPath 
 *   待上传的voide 的path 
 * @param title 
 *   视频标题 
 * @param introduction 
 *   视频描述 
 * @return 
 * @throws Exception 
 */ 
public static String HttpsUploadVideoMediaFile(String method, String path, Map param, 
  String mediaPath, String title, String introduction) throws Exception { 
 String result = null; 
 URL url = new URL(setParmas(param, path, "")); 
 OutputStream output = null; 
 DataInputStream inputStream = null; 
 try { 
  File file = new File(mediaPath); 
  if (!file.isFile() || !file.exists()) { 
   throw new IOException("file is not exist"); 
  } 
  HttpURLConnection con = (HttpURLConnection) url.openConnection(); 
  con.setDoInput(true); 
  con.setDoOutput(true); 
  con.setUseCaches(false); 
  con.setRequestMethod(SystemConfig.POST_METHOD); 
  // 设置请求头信息 
  con.setRequestProperty("Connection", "Keep-Alive"); 
  con.setRequestProperty("Charset", SystemConfig.DEFAULT_CHARACTER_ENCODING); 
  // 设置边界 
  String boundary = "----------" + System.currentTimeMillis(); 
  con.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); 
  // 请求正文信息 
  // 第一部分 
  output = new DataOutputStream(con.getOutputStream()); 
  IOUtils.write(("--" + boundary + "\r\n").getBytes(SystemConfig.DEFAULT_CHARACTER_ENCODING), output); 
  IOUtils.write(("Content-Disposition: form-data;name=\"media\"; filename=\"" + file.getName() + "\"\r\n") 
    .getBytes(), output); 
  IOUtils.write("Content-Type: video/mp4 \r\n\r\n".getBytes(), output); 
  // 文件正文部分 
  // 把文件已流文件的方式 推入到url中 
  inputStream = new DataInputStream(new FileInputStream(file)); 
  IOUtils.copy(inputStream, output); 
  // 结尾部分 
  IOUtils.write(("--" + boundary + "\r\n").getBytes(SystemConfig.DEFAULT_CHARACTER_ENCODING), output); 
  IOUtils.write("Content-Disposition: form-data; name=\"description\";\r\n\r\n" 
    .getBytes(SystemConfig.DEFAULT_CHARACTER_ENCODING), output); 
  IOUtils.write(("{\"title\":\"" + title + "\",\"introduction\":\"" + introduction + "\"}") 
    .getBytes(SystemConfig.DEFAULT_CHARACTER_ENCODING), output); 
  IOUtils.write(("\r\n--" + boundary + "--\r\n\r\n").getBytes(SystemConfig.DEFAULT_CHARACTER_ENCODING), 
    output); 
  output.flush(); 
  result = inputStreamToString(con.getInputStream()); 
 } catch (MalformedURLException e) { 
  e.printStackTrace(); 
 } catch (ProtocolException e) { 
  e.printStackTrace(); 
 } catch (IOException e) { 
  throw new IOException("read data error"); 
 } finally { 
  IOUtils.closeQuietly(output); 
  IOUtils.closeQuietly(inputStream); 
 } 
 return result; 
} 
 
/** 
 * 上传Video媒体文件(网络) 
 * 
 * @param method 
 *   请求方法 GET/POST 
 * @param path 
 *   api的路径 
 * @param param 
 *   api参数 
 * @param mediaPath 
 *   待上传的voide 的path 
 * @param title 
 *   视频标题 
 * @param introduction 
 *   视频描述 
 * @param connTime 
 *   连接时间 默认为5000 
 * @param readTime 
 *   读取时间 默认为5000 
 * @return 
 * @throws Exception 
 */ 
public static String HttpsUploadVideoMedia(String method, String path, Map param, String mediaPath, 
  String title, String introduction, int connTime, int readTime) throws Exception { 
 String result = null; 
 URL url = new URL(setParmas(param, path, "")); 
 OutputStream output = null; 
 BufferedInputStream inputStream = null; 
 try { 
  String boundary = "----"; 
  HttpURLConnection conn = getConnection(method, url); 
  conn.setConnectTimeout(connTime == 0 ? DEFAULT_CONNTIME : connTime); 
  conn.setReadTimeout(readTime == 0 ? DEFAULT_UPLOAD_READTIME : readTime); 
  conn.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary); 
  output = conn.getOutputStream(); 
  URL mediaUrl = new URL(mediaPath); 
  if (mediaUrl != null) { 
   HttpURLConnection mediaConn = (HttpURLConnection) mediaUrl.openConnection(); 
   mediaConn.setDoOutput(true); 
   mediaConn.setUseCaches(false); 
   mediaConn.setRequestMethod(SystemConfig.GET_METHOD); 
   mediaConn.setConnectTimeout(connTime == 0 ? DEFAULT_CONNTIME : connTime); 
   mediaConn.setReadTimeout(readTime == 0 ? DEFAULT_UPLOAD_READTIME : readTime); 
   IOUtils.write(("--" + boundary + "\r\n").getBytes(), output); 
   IOUtils.write(("Content-Disposition: form-data; name=\"media\"; filename=\"" + getFileName(mediaPath) 
     + "\"\r\n").getBytes(), output); 
   IOUtils.write("Content-Type: video/mp4 \r\n\r\n".getBytes(), output); 
   inputStream = new BufferedInputStream(mediaConn.getInputStream()); 
   IOUtils.copy(inputStream, output); 
   // 结尾部分 
   IOUtils.write(("--" + boundary + "\r\n").getBytes(SystemConfig.DEFAULT_CHARACTER_ENCODING), output); 
   IOUtils.write("Content-Disposition: form-data; name=\"description\";\r\n\r\n" 
     .getBytes(SystemConfig.DEFAULT_CHARACTER_ENCODING), output); 
   IOUtils.write(("{\"title\":\"" + title + "\",\"introduction\":\"" + introduction + "\"}") 
     .getBytes(SystemConfig.DEFAULT_CHARACTER_ENCODING), output); 
   IOUtils.write(("\r\n--" + boundary + "--\r\n\r\n").getBytes(SystemConfig.DEFAULT_CHARACTER_ENCODING), 
     output); 
   mediaConn.disconnect(); 
   // 获取输入流 
   result = inputStreamToString(conn.getInputStream()); 
  } 
 } catch (MalformedURLException e) { 
  e.printStackTrace(); 
 } catch (ProtocolException e) { 
  e.printStackTrace(); 
 } catch (IOException e) { 
  throw new IOException("read data error"); 
 } finally { 
  IOUtils.closeQuietly(output); 
  IOUtils.closeQuietly(inputStream); 
 } 
 return result; 
} 

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持创新互联。


分享标题:Java微信公众平台之素材管理
链接地址:http://cdweb.net/article/gsedsg.html